[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Create a report for bugs or other issues you've encountered.\nlabels: [ bug ]\nbody:\n  - type: textarea\n    id: summary\n    attributes:\n      label: Problem description\n      description: A clear and concise description of the issue.\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduce-steps\n    attributes:\n      label: Steps to reproduce\n      description: Describe the steps to reproduce the issue.\n      placeholder: |\n        Example:\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n\n  - type: input\n    id: app-version\n    attributes:\n      label: App version\n      description: The installed app version (Settings > About).\n      placeholder: 1.0.0\n    validations:\n      required: true\n\n  - type: input\n    id: android-version\n    attributes:\n      label: Android version\n      description: The Android version of your device.\n      placeholder: \"10\"\n    validations:\n      required: true\n\n  - type: input\n    id: device\n    attributes:\n      label: Device information\n      description: |\n        Optional: The device model you're using.\n      placeholder: e.g. Google Pixel 4\n\n  - type: textarea\n    id: attachments\n    attributes:\n      label: Attachments and Logs\n      description: |\n        If applicable, add screenshots or screen recordings to help explain the issue.\n        You can also paste or upload logs here (you can find them in Settings > Application Logs).\n\n  - type: checkboxes\n    id: acknowledgements\n    attributes:\n      label: Acknowledgements\n      options:\n        - label: I have searched the open and closed [issues](https://github.com/Drumber/Kitsune/issues?q=is%3Aissue) and this is **NOT** a duplicate.\n          required: true\n        - label: I'm using the latest version of the app.\n          required: true\n        - label: I have provided all required information.\n          required: true"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest new features or improvements for the app.\nlabels: [ enhancement ]\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: Describe the feature you'd like to see.\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: acknowledgements\n    attributes:\n      label: Acknowledgements\n      options:\n        - label: I have searched the open and closed [issues](https://github.com/Drumber/Kitsune/issues?q=is%3Aissue) and this is **NOT** a duplicate.\n          required: true"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build app\n\non:\n  workflow_dispatch:\n    inputs:\n      buildVariant:\n        description: 'App build variant'\n        required: false\n        default: 'release'\n  workflow_call:\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n      - name: Set gradle build task\n        run: |\n          if [ -z \"${{ github.event.inputs.buildVariant }}\" ]; then\n            echo \"build_task='assembleRelease'\" >> \"$GITHUB_ENV\"\n          else\n            echo \"build_task=assemble${{ github.event.inputs.buildVariant }}\" >> \"$GITHUB_ENV\"\n          fi\n      - name: Build with Gradle\n        uses: gradle/gradle-build-action@v2\n        with:\n          arguments: ${{ env.build_task }}\n      - name: Copy apk files\n        run: mkdir -p ./artifacts && cp app/build/outputs/apk/**/*.apk ./artifacts/\n      - name: Upload apk\n        uses: actions/upload-artifact@v4\n        with:\n          name: app-unsigned\n          path: artifacts/*.apk\n          if-no-files-found: error\n"
  },
  {
    "path": ".github/workflows/reproducible-build.yml",
    "content": "name: Verify reproducible build\n\non:\n  workflow_dispatch:\n    inputs:\n      releaseTag:\n        description: Tag of the release to download\n        required: true\n  release:\n    types: [ published ]\n\npermissions:\n  contents: write\n\njobs:\n  build:\n    uses: ./.github/workflows/build.yml\n\n  verify:\n    needs: [ build ]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Install dependencies\n        run: sudo apt-get update && sudo apt-get install apksigner python3-click apksigcopier -y\n      - name: Download build artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: app-unsigned\n      - run: mv *.apk unsigned.apk\n      - name: Set asset URL\n        id: set_asset_url\n        run: |\n          if [ \"${{ github.event_name }}\" = \"release\" ]; then\n            echo \"release_tag=${{ github.event.release.tag_name }}\" >> \"$GITHUB_ENV\"\n          else\n            echo \"release_tag=${{ github.event.inputs.releaseTag }}\" >> \"$GITHUB_ENV\"\n          fi\n      - name: Download release asset\n        run: |\n          gh release download \"$release_tag\" --pattern \"*.apk\" --output upstream.apk --repo \"$REPO\"\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          REPO: ${{ github.repository }}\n      - name: Compare APKs\n        run: apksigcopier compare upstream.apk --unsigned unsigned.apk && echo OK\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test app\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up JDK\n      uses: actions/setup-java@v3\n      with:\n        java-version: '17'\n        distribution: 'temurin'\n    - name: Test with Gradle\n      uses: gradle/gradle-build-action@v2\n      with:\n        arguments: testDebugUnitTest\n"
  },
  {
    "path": ".gitignore",
    "content": "# Built application files\n*.apk\n*.aar\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n#  Uncomment the following line in case you need and you don't have the release build type files in your app\n# release/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# IntelliJ\n*.iml\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/gradle.xml\n.idea/assetWizardSettings.xml\n.idea/dictionaries\n.idea/libraries\n.idea/jarRepositories.xml\n.idea/deploymentTargetDropDown.xml\n.idea/misc.xml\n# Android Studio 3 in .gitignore file.\n.idea/caches\n.idea/modules.xml\n# Comment next line if keeping position of elements in Navigation Editor is relevant for you\n.idea/navEditor.xml\n\n# Keystore files\n# Uncomment the following lines if you do not want to check your keystore files in.\n#*.jks\n#*.keystore\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n.cxx/\n\n# Google Services (e.g. APIs or Firebase)\n# google-services.json\n\n# Freeline\nfreeline.py\nfreeline/\nfreeline_project_description.json\n\n# fastlane\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots\nfastlane/test_output\nfastlane/readme.md\n\n# Ruby bundler\n/.bundle/\n\n# Version control\nvcs.xml\n\n# lint\nlint/intermediates/\nlint/generated/\nlint/outputs/\nlint/tmp/\n# lint/reports/\n\n# Android Profiling\n*.hprof\n\n"
  },
  {
    "path": ".idea/.gitignore",
    "content": "# Default ignored files\n/shelf/\n/workspace.xml\n/androidTestResultsUserPreferences.xml\n/deploymentTargetSelector.xml\n/inspectionProfiles/Project_Default.xml\n/runConfigurations.xml\n/studiobot.xml\n/deviceManager.xml\n"
  },
  {
    "path": ".idea/AndroidProjectSystem.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"AndroidProjectSystem\">\n    <option name=\"providerId\" value=\"com.android.tools.idea.GradleProjectSystem\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <option name=\"OTHER_INDENT_OPTIONS\">\n      <value>\n        <option name=\"INDENT_SIZE\" value=\"2\" />\n        <option name=\"TAB_SIZE\" value=\"2\" />\n      </value>\n    </option>\n    <JetCodeStyleSettings>\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n    </JetCodeStyleSettings>\n    <codeStyleSettings language=\"XML\">\n      <option name=\"FORCE_REARRANGE_MODE\" value=\"1\" />\n      <indentOptions>\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n      </indentOptions>\n      <arrangement>\n        <rules>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>xmlns:android</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>xmlns:.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:id</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:name</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>name</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>style</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>ANDROID_ATTRIBUTE_ORDER</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>.*</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n        </rules>\n      </arrangement>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"kotlin\">\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n    </codeStyleSettings>\n  </code_scheme>\n</component>"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n  </state>\n</component>"
  },
  {
    "path": ".idea/compiler.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"CompilerConfiguration\">\n    <bytecodeTargetLevel target=\"17\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/kotlinc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"KotlinJpsPluginSettings\">\n    <option name=\"version\" value=\"2.2.20\" />\n  </component>\n</project>"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "\n# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, caste, color, religion, or sexual\nidentity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the overall\n  community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or advances of\n  any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email address,\n  without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official email address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the project team by [opening an new issue](https://github.com/Drumber/Kitsune/issues/new/choose).\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of\nactions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or permanent\nban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the\ncommunity.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.1, available at\n[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].\n\nCommunity Impact Guidelines were inspired by\n[Mozilla's code of conduct enforcement ladder][Mozilla CoC].\n\nFor answers to common questions about this code of conduct, see the FAQ at\n[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at\n[https://www.contributor-covenant.org/translations][translations].\n\n[homepage]: https://www.contributor-covenant.org\n[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html\n[Mozilla CoC]: https://github.com/mozilla/diversity\n[FAQ]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Kitsune\nThank you for your interest in contributing to Kitsune! Any support is greatly appreciated.\n\n## Ways to Contribute\nAny contributions are welcomed, including but not limited to:\n\n### 1. Code Contributions\n  - Check out the [open issues](https://github.com/Drumber/Kitsune/issues) or the [project board](https://github.com/users/Drumber/projects/2) and pick one to work on.\n  - See [How to Contribute Code Changes](#how-to-contribute-code-changes) to get started.\n  - Submit a pull request with your changes.\n\n### 2. Image assets and Logos\nInterested in making Kitsune more visually appealing?\nDon't hesitate to open a [new issue](https://github.com/Drumber/Kitsune/issues/new/choose) or [discussion](https://github.com/Drumber/Kitsune/discussions/new/choose) if you want to create:\n  - custom placeholder images for media and user banners, posters or character photos\n  - a new app icon\n  - or any other design contribution.\n\n### 3. Translation\n  - Contribute translations for different languages to make the app accessible globally.\n  - Copy the [string.xml](app/src/main/res/values/strings.xml) file to a new folder named `values-LANG` (e.g. `values-fr` for French) inside the [res](app/src/main/res) directory.\n    - Remove strings with `translatable=\"false\"`.\n    - Keep placeholders like `%s` or `%d` in the strings.\n  - Submit a pull request with your changes.\n\n### 4. Documentation and Repository Files\n  - Improve the documentation or contribute to other repository files, like issue templates.\n\n## How to Contribute Code Changes\nTo start developing, follow these simple steps:\n\n1. **Set Up Your Environment:**\n  - Make sure you have [Android Studio](https://developer.android.com/studio) installed.\n  - Familiarize yourself with [Kotlin](https://kotlinlang.org/) as it's the primary language used in the app.\n\n2. **Clone the Repository**\n\n3. **Build and Run:**\n  - Open the project in Android Studio.\n  - Build and run the app to ensure everything is set up correctly.\n"
  },
  {
    "path": "Gemfile",
    "content": "source \"https://rubygems.org\"\n\ngem \"fastlane\"\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<img src=\"./media/kitsune-logo.svg\">\n<h1>Kitsune</h1>\n</div>\n\nUnofficial android app for [Kitsu](https://kitsu.app). Discover new anime and manga and manage your library.\n\n## Features\n- Explore and search anime and manga, even without an account\n- View anime and manga details including episodes/chapters and characters\n- Manage your Kitsu library and account settings\n- Cached library for offline use\n- Multiple dark and light app themes\n- Material 3 Design\n- Home screen widget\n\n#### Missing features\n- Reactions/Comments\n- Global message feed and announcements\n- Groups\n- Search for other users\n\n## Download\n> Requires Android 8.0 or higher.\n\nKitsune is available for download on GitHub and F-Droid.\n\n[Download latest app release on GitHub.](https://github.com/Drumber/Kitsune/releases/latest)\n\n[<img src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on.png\" alt=\"Get it on F-Droid\" height=\"75\">](https://f-droid.org/packages/io.github.drumber.kitsune/)\n\n## Localization\nAre you interested in translating Kitsune into your language?  \nHead over to the Kitsune project on [Hosted Weblate](https://hosted.weblate.org/engage/kitsune/) and help localize the app.\n\n[![Translation status](https://hosted.weblate.org/widget/kitsune/multi-auto.svg)](https://hosted.weblate.org/engage/kitsune/)\n\n## Bug reports, Feature requests and Contribution\n> Please be aware of the [Code of Conduct](CODE_OF_CONDUCT.md) in place.\n\n**Report a bug or request a new feature**\n- Please check out [existing issues](https://github.com/Drumber/Kitsune/issues?q=is%3Aissue) first to avoid duplicates.\n- [Open a new issue](https://github.com/Drumber/Kitsune/issues/new/choose)\n\n**Contribute to Kitsune**\n- See [Contributing](CONTRIBUTING.md) for more details.\n\n## Screenshots\n<img src=\"./media/light_home_screen_framed.png\" width=\"250\"> <img src=\"./media/dark_home_screen_framed.png\" width=\"250\"> <img src=\"./media/dark_purple_home_screen_framed.png\" width=\"250\">\n\n<img src=\"./media/dark_details_screen_framed.png\" width=\"250\"> <img src=\"./media/light_details_ratings_screen_framed.png\" width=\"250\">\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n/release"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.dsl.KotlinVersion\n\nplugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.compose.compiler)\n    alias(libs.plugins.android.legacy.kapt)\n    alias(libs.plugins.ksp)\n    alias(libs.plugins.androidx.navigation.safeargs)\n    alias(libs.plugins.aboutlibraries.plugin)\n    alias(libs.plugins.jetbrains.kotlin.parcelize)\n    alias(libs.plugins.jetbrains.kotlin.serialization)\n    id(\"kitsune-plugin\")\n}\n\nval screenshotMode: String by project\n\nandroid {\n    namespace = \"io.github.drumber.kitsune\"\n    compileSdk = 36\n    buildToolsVersion = \"36.0.0\"\n\n    defaultConfig {\n        applicationId = \"io.github.drumber.kitsune\"\n        minSdk = 26\n        targetSdk = 35\n        versionCode = 39\n        versionName = \"2.0.6\"\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n\n        buildConfigField(\"boolean\", \"SCREENSHOT_MODE_ENABLED\", screenshotMode)\n        buildConfigField(\"boolean\", \"INSTRUMENTED_TEST\", \"false\")\n    }\n\n    androidResources {\n        generateLocaleConfig = true\n    }\n\n    buildTypes {\n        getByName(\"release\") {\n            isMinifyEnabled = true\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n            signingConfig = signingConfigs.getByName(\"debug\")\n            vcsInfo.include = false\n        }\n\n        getByName(\"debug\") {\n            applicationIdSuffix = \".debug\"\n            versionNameSuffix = \"-debug\"\n            isDebuggable = true\n        }\n\n        create(\"instrumented\") {\n            initWith(getByName(\"debug\"))\n            applicationIdSuffix = \".instrumented\"\n            buildConfigField(\"boolean\", \"INSTRUMENTED_TEST\", \"true\")\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_11\n        targetCompatibility = JavaVersion.VERSION_11\n    }\n\n    buildFeatures {\n        viewBinding = true\n        dataBinding = true\n        buildConfig = true\n        compose = true\n    }\n\n    packaging {\n        resources.excludes += \"META-INF/*.kotlin_module\"\n    }\n\n    dependenciesInfo {\n        includeInApk = false\n        includeInBundle = false\n    }\n\n    testOptions {\n        animationsDisabled = true\n        testBuildType = \"instrumented\"\n    }\n}\n\nkotlin {\n    compilerOptions {\n        jvmTarget = JvmTarget.JVM_11\n        languageVersion = KotlinVersion.KOTLIN_2_2\n        freeCompilerArgs.add(\"-opt-in=kotlin.RequiresOptIn\")\n    }\n}\n\nksp {\n    arg(\"room.schemaLocation\", \"$projectDir/schemas\")\n}\n\naboutLibraries {\n    offlineMode = true\n    // Remove the \"generated\" timestamp to allow for reproducible builds\n    excludeFields = arrayOf(\"generated\")\n}\n\ndependencies {\n    // Android core and support libs\n    implementation(libs.androidx.core.ktx)\n    implementation(libs.androidx.appcompat)\n    implementation(libs.androidx.constraint.layout)\n    implementation(libs.androidx.core.splashscreen)\n\n    // Compose\n    val composeBom = platform(libs.androidx.compose.bom)\n    implementation(composeBom)\n    implementation(libs.androidx.activity.compose)\n    implementation(libs.androidx.compose.material3)\n    implementation(libs.androidx.compose.material3.adaptive)\n    implementation(libs.accompanist.themeadapter.material3)\n    implementation(libs.accompanist.permissions)\n    implementation(libs.androidx.compose.ui.tooling.preview)\n    debugImplementation(libs.androidx.compose.ui.tooling)\n\n    // SwipeRefresh layout\n    implementation(libs.androidx.swiperefreshlayout)\n\n    // Navigation\n    implementation(libs.androidx.navigation.fragment.ktx)\n    implementation(libs.androidx.navigation.ui.ktx)\n    implementation(libs.androidx.fragment.ktx)\n\n    // Preference\n    implementation(libs.androidx.preference.ktx)\n\n    // Lifecycle\n    implementation(libs.androidx.lifecycle.viewmodel.ktx)\n    implementation(libs.androidx.lifecycle.livedata.ktx)\n\n    // WorkManager\n    implementation(libs.androidx.workmanager)\n\n    // Material\n    implementation(libs.google.android.material)\n\n    // Glance AppWidget\n    implementation(libs.androidx.glance.appwidget)\n    implementation(libs.androidx.glance.material3)\n    implementation(libs.androidx.glance.preview)\n\n    // Kotlin coroutines\n    implementation(libs.jetbrains.kotlinx.coroutines.core)\n    implementation(libs.jetbrains.kotlinx.coroutines.android)\n\n    // Paging\n    implementation(libs.androidx.paging.runtime.ktx)\n\n    // Room\n    implementation(libs.androidx.room.runtime)\n    implementation(libs.androidx.room.ktx)\n    ksp(libs.androidx.room.compiler)\n    implementation(libs.androidx.room.paging)\n\n    // ViewPager\n    implementation(libs.androidx.viewpager2)\n\n    // Glide\n    implementation(libs.bumptech.glide)\n    ksp(libs.bumptech.glide.ksp)\n    implementation(libs.bumptech.glide.okhttp3)\n    implementation(libs.bumptech.glide.compose)\n\n    // Koin DI\n    implementation(libs.insert.koin.android)\n    implementation(libs.insert.koin.androidx.navigation)\n\n    // jsonapi-converter\n    implementation(libs.jasminb.jsonapi)\n\n    // Jackson\n    implementation(libs.fasterxml.jackson.databind)\n    implementation(libs.fasterxml.jackson.kotlin)\n\n    // Retrofit\n    implementation(libs.squareup.retrofit2.retrofit)\n    implementation(libs.squareup.retrofit2.jackson)\n\n    // OkHttp\n    implementation(libs.squareup.okhttp3.okhttp)\n    implementation(libs.squareup.okhttp3.logging)\n\n    // Algolia Instantsearch\n    implementation(libs.algolia.instantsearch.android)\n    implementation(libs.algolia.instantsearch.android.paging3)\n    implementation(libs.algolia.instantsearch.coroutines)\n\n    // Kotlinx serialization\n    implementation(libs.jetbrains.kotlinx.serialization)\n\n    // Ktor client\n    implementation(libs.ktor.client.okhttp)\n\n    // Kotpref\n    implementation(libs.chibatching.kotpref)\n    implementation(libs.chibatching.kotpref.enum)\n    implementation(libs.chibatching.kotpref.livedata)\n\n    // Security Crypto\n    implementation(libs.androidx.security.crypto)\n\n    // TreeView\n    implementation(libs.bmelnychuk.treeview)\n\n    // Expandable text view\n    implementation(libs.blogc.expandabletextview)\n\n    // CircleImageView\n    implementation(libs.hdodenhof.circleimageview)\n\n    // Material Rating Bar\n    implementation(libs.zhanghai.materialratingbar)\n\n    // MPAndroidCharts\n    implementation(libs.philjay.mpandroidchart)\n\n    // Photo View\n    implementation(libs.chrisbanes.photoview)\n\n    // Hauler Gesture\n    implementation(libs.futured.hauler)\n    implementation(libs.futured.hauler.databinding)\n\n    // AboutLibraries\n    implementation(libs.mikepenz.aboutlibraries.core)\n    implementation(libs.mikepenz.aboutlibraries)\n\n    // LeakCanary\n    debugImplementation(libs.squareup.leakcanary)\n\n    // Glide Transformations (only used for demo screenshots)\n    if (screenshotMode.toBoolean()) {\n        implementation(libs.wasabeef.glide.transformations)\n    }\n\n    // Tests\n    testImplementation(libs.junit)\n    testImplementation(libs.assertj.core)\n    testImplementation(libs.tngtech.archunit.junit4)\n    testImplementation(libs.robolectric)\n    androidTestImplementation(libs.androidx.junit)\n    androidTestImplementation(libs.androidx.junit.ktx)\n    androidTestImplementation(libs.androidx.test.rules)\n    androidTestImplementation(libs.androidx.espresso.core)\n    androidTestImplementation(libs.androidx.espresso.contrib)\n\n    // Compose tests\n    androidTestImplementation(composeBom)\n    androidTestImplementation(libs.androidx.compose.ui.test)\n    debugImplementation(libs.androidx.compose.ui.test.manifest)\n\n    testImplementation(libs.jetbrains.kotlinx.coroutines.test)\n    testImplementation(libs.insert.koin.test.junit4)\n    testImplementation(libs.mockito.kotlin)\n    testImplementation(libs.datafaker)\n\n    // fastlane screengrab\n    androidTestImplementation(libs.fastlane.screengrab)\n}"
  },
  {
    "path": "app/gradle.properties",
    "content": "# set to 'true' to apply blur effect to images (note: build target must be 'debug')\nscreenshotMode=false"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n#-printusage r8-report/usage.txt\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n\n-dontobfuscate\n\n# General\n-keepattributes SourceFile,LineNumberTable,Signature,*Annotation*,EnclosingMethod,Exceptions,InnerClasses\n\n# Kotlin reflection\n-keep class kotlin.Metadata { *; }\n\n# Slf4j\n-dontwarn org.slf4j.impl.StaticLoggerBinder\n-dontwarn org.slf4j.impl.StaticMDCBinder\n\n# Jackson\n-keepnames class com.fasterxml.jackson.** { *; }\n-keepclassmembers class * {\n     @com.fasterxml.jackson.annotation.* *;\n}\n-dontwarn com.fasterxml.jackson.databind.**\n\n# jsonapi-converter\n-keepclassmembers class * {\n    @com.github.jasminb.jsonapi.annotations.* *;\n}\n-keep class * implements com.github.jasminb.jsonapi.ResourceIdHandler\n\n# MPAndroidChart\n-keep class com.github.mikephil.charting.** { *; }\n\n############################################\n# Kitsune specific rules\n############################################\n\n# keep all classes\n-keep class io.github.drumber.kitsune.** { *; }\n\n# keep search filters\n-keep class io.github.drumber.kitsune.domain.algolia.FilterCollectionEntry** { *; }\n-keep class com.algolia.instantsearch.filter.state.FilterGroupID** { *; }\n-keep class com.algolia.instantsearch.filter.state.Filters** { *; }\n-keep class com.algolia.search.model.filter.Filter** { *; }\n-keep class com.algolia.search.model.Attribute** { *; }\n"
  },
  {
    "path": "app/schemas/io.github.drumber.kitsune.data.source.local.LocalDatabase/1.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"9ffeea6c7e014697eeeb8de0dbec5a99\",\n    \"entities\": [\n      {\n        \"tableName\": \"library_entries\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `updatedAt` TEXT, `startedAt` TEXT, `finishedAt` TEXT, `progressedAt` TEXT, `status` INTEGER, `progress` INTEGER, `reconsuming` INTEGER, `reconsumeCount` INTEGER, `volumesOwned` INTEGER, `ratingTwenty` INTEGER, `notes` TEXT, `privateEntry` INTEGER, `reactionSkipped` TEXT, `anime_id` TEXT, `anime_slug` TEXT, `anime_description` TEXT, `anime_titles` TEXT, `anime_canonicalTitle` TEXT, `anime_abbreviatedTitles` TEXT, `anime_averageRating` TEXT, `anime_userCount` INTEGER, `anime_favoritesCount` INTEGER, `anime_popularityRank` INTEGER, `anime_ratingRank` INTEGER, `anime_startDate` TEXT, `anime_endDate` TEXT, `anime_nextRelease` TEXT, `anime_tba` TEXT, `anime_status` TEXT, `anime_ageRating` TEXT, `anime_ageRatingGuide` TEXT, `anime_nsfw` INTEGER, `anime_totalLength` INTEGER, `anime_episodeCount` INTEGER, `anime_episodeLength` INTEGER, `anime_youtubeVideoId` TEXT, `anime_subtype` TEXT, `anime_rating_r2` TEXT, `anime_rating_r3` TEXT, `anime_rating_r4` TEXT, `anime_rating_r5` TEXT, `anime_rating_r6` TEXT, `anime_rating_r7` TEXT, `anime_rating_r8` TEXT, `anime_rating_r9` TEXT, `anime_rating_r10` TEXT, `anime_rating_r11` TEXT, `anime_rating_r12` TEXT, `anime_rating_r13` TEXT, `anime_rating_r14` TEXT, `anime_rating_r15` TEXT, `anime_rating_r16` TEXT, `anime_rating_r17` TEXT, `anime_rating_r18` TEXT, `anime_rating_r19` TEXT, `anime_rating_r20` TEXT, `anime_poster_tiny` TEXT, `anime_poster_small` TEXT, `anime_poster_medium` TEXT, `anime_poster_large` TEXT, `anime_poster_original` TEXT, `anime_poster_meta_tiny_width` INTEGER, `anime_poster_meta_tiny_height` INTEGER, `anime_poster_meta_small_width` INTEGER, `anime_poster_meta_small_height` INTEGER, `anime_poster_meta_medium_width` INTEGER, `anime_poster_meta_medium_height` INTEGER, `anime_poster_meta_large_width` INTEGER, `anime_poster_meta_large_height` INTEGER, `anime_cover_tiny` TEXT, `anime_cover_small` TEXT, `anime_cover_medium` TEXT, `anime_cover_large` TEXT, `anime_cover_original` TEXT, `anime_cover_meta_tiny_width` INTEGER, `anime_cover_meta_tiny_height` INTEGER, `anime_cover_meta_small_width` INTEGER, `anime_cover_meta_small_height` INTEGER, `anime_cover_meta_medium_width` INTEGER, `anime_cover_meta_medium_height` INTEGER, `anime_cover_meta_large_width` INTEGER, `anime_cover_meta_large_height` INTEGER, `manga_id` TEXT, `manga_slug` TEXT, `manga_description` TEXT, `manga_titles` TEXT, `manga_canonicalTitle` TEXT, `manga_abbreviatedTitles` TEXT, `manga_averageRating` TEXT, `manga_userCount` INTEGER, `manga_favoritesCount` INTEGER, `manga_popularityRank` INTEGER, `manga_ratingRank` INTEGER, `manga_startDate` TEXT, `manga_endDate` TEXT, `manga_nextRelease` TEXT, `manga_tba` TEXT, `manga_status` TEXT, `manga_ageRating` TEXT, `manga_ageRatingGuide` TEXT, `manga_nsfw` INTEGER, `manga_totalLength` INTEGER, `manga_chapterCount` INTEGER, `manga_volumeCount` INTEGER, `manga_subtype` TEXT, `manga_serialization` TEXT, `manga_rating_r2` TEXT, `manga_rating_r3` TEXT, `manga_rating_r4` TEXT, `manga_rating_r5` TEXT, `manga_rating_r6` TEXT, `manga_rating_r7` TEXT, `manga_rating_r8` TEXT, `manga_rating_r9` TEXT, `manga_rating_r10` TEXT, `manga_rating_r11` TEXT, `manga_rating_r12` TEXT, `manga_rating_r13` TEXT, `manga_rating_r14` TEXT, `manga_rating_r15` TEXT, `manga_rating_r16` TEXT, `manga_rating_r17` TEXT, `manga_rating_r18` TEXT, `manga_rating_r19` TEXT, `manga_rating_r20` TEXT, `manga_poster_tiny` TEXT, `manga_poster_small` TEXT, `manga_poster_medium` TEXT, `manga_poster_large` TEXT, `manga_poster_original` TEXT, `manga_poster_meta_tiny_width` INTEGER, `manga_poster_meta_tiny_height` INTEGER, `manga_poster_meta_small_width` INTEGER, `manga_poster_meta_small_height` INTEGER, `manga_poster_meta_medium_width` INTEGER, `manga_poster_meta_medium_height` INTEGER, `manga_poster_meta_large_width` INTEGER, `manga_poster_meta_large_height` INTEGER, `manga_cover_tiny` TEXT, `manga_cover_small` TEXT, `manga_cover_medium` TEXT, `manga_cover_large` TEXT, `manga_cover_original` TEXT, `manga_cover_meta_tiny_width` INTEGER, `manga_cover_meta_tiny_height` INTEGER, `manga_cover_meta_small_width` INTEGER, `manga_cover_meta_small_height` INTEGER, `manga_cover_meta_medium_width` INTEGER, `manga_cover_meta_medium_height` INTEGER, `manga_cover_meta_large_width` INTEGER, `manga_cover_meta_large_height` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updatedAt\",\n            \"columnName\": \"updatedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"startedAt\",\n            \"columnName\": \"startedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"finishedAt\",\n            \"columnName\": \"finishedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progressedAt\",\n            \"columnName\": \"progressedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progress\",\n            \"columnName\": \"progress\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsuming\",\n            \"columnName\": \"reconsuming\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsumeCount\",\n            \"columnName\": \"reconsumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"volumesOwned\",\n            \"columnName\": \"volumesOwned\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ratingTwenty\",\n            \"columnName\": \"ratingTwenty\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"notes\",\n            \"columnName\": \"notes\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"privateEntry\",\n            \"columnName\": \"privateEntry\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reactionSkipped\",\n            \"columnName\": \"reactionSkipped\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.id\",\n            \"columnName\": \"anime_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.slug\",\n            \"columnName\": \"anime_slug\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.description\",\n            \"columnName\": \"anime_description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.titles\",\n            \"columnName\": \"anime_titles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.canonicalTitle\",\n            \"columnName\": \"anime_canonicalTitle\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.abbreviatedTitles\",\n            \"columnName\": \"anime_abbreviatedTitles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.averageRating\",\n            \"columnName\": \"anime_averageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.userCount\",\n            \"columnName\": \"anime_userCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.favoritesCount\",\n            \"columnName\": \"anime_favoritesCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.popularityRank\",\n            \"columnName\": \"anime_popularityRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingRank\",\n            \"columnName\": \"anime_ratingRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.startDate\",\n            \"columnName\": \"anime_startDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.endDate\",\n            \"columnName\": \"anime_endDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.nextRelease\",\n            \"columnName\": \"anime_nextRelease\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.tba\",\n            \"columnName\": \"anime_tba\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.status\",\n            \"columnName\": \"anime_status\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ageRating\",\n            \"columnName\": \"anime_ageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ageRatingGuide\",\n            \"columnName\": \"anime_ageRatingGuide\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.nsfw\",\n            \"columnName\": \"anime_nsfw\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.totalLength\",\n            \"columnName\": \"anime_totalLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.episodeCount\",\n            \"columnName\": \"anime_episodeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.episodeLength\",\n            \"columnName\": \"anime_episodeLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.youtubeVideoId\",\n            \"columnName\": \"anime_youtubeVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.subtype\",\n            \"columnName\": \"anime_subtype\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r2\",\n            \"columnName\": \"anime_rating_r2\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r3\",\n            \"columnName\": \"anime_rating_r3\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r4\",\n            \"columnName\": \"anime_rating_r4\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r5\",\n            \"columnName\": \"anime_rating_r5\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r6\",\n            \"columnName\": \"anime_rating_r6\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r7\",\n            \"columnName\": \"anime_rating_r7\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r8\",\n            \"columnName\": \"anime_rating_r8\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r9\",\n            \"columnName\": \"anime_rating_r9\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r10\",\n            \"columnName\": \"anime_rating_r10\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r11\",\n            \"columnName\": \"anime_rating_r11\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r12\",\n            \"columnName\": \"anime_rating_r12\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r13\",\n            \"columnName\": \"anime_rating_r13\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r14\",\n            \"columnName\": \"anime_rating_r14\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r15\",\n            \"columnName\": \"anime_rating_r15\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r16\",\n            \"columnName\": \"anime_rating_r16\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r17\",\n            \"columnName\": \"anime_rating_r17\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r18\",\n            \"columnName\": \"anime_rating_r18\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r19\",\n            \"columnName\": \"anime_rating_r19\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r20\",\n            \"columnName\": \"anime_rating_r20\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.tiny\",\n            \"columnName\": \"anime_poster_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.small\",\n            \"columnName\": \"anime_poster_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.medium\",\n            \"columnName\": \"anime_poster_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.large\",\n            \"columnName\": \"anime_poster_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.original\",\n            \"columnName\": \"anime_poster_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"anime_poster_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"anime_poster_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.small.width\",\n            \"columnName\": \"anime_poster_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.small.height\",\n            \"columnName\": \"anime_poster_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.medium.width\",\n            \"columnName\": \"anime_poster_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.medium.height\",\n            \"columnName\": \"anime_poster_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.large.width\",\n            \"columnName\": \"anime_poster_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.large.height\",\n            \"columnName\": \"anime_poster_meta_large_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.tiny\",\n            \"columnName\": \"anime_cover_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.small\",\n            \"columnName\": \"anime_cover_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.medium\",\n            \"columnName\": \"anime_cover_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.large\",\n            \"columnName\": \"anime_cover_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.original\",\n            \"columnName\": \"anime_cover_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"anime_cover_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"anime_cover_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.small.width\",\n            \"columnName\": \"anime_cover_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.small.height\",\n            \"columnName\": \"anime_cover_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.medium.width\",\n            \"columnName\": \"anime_cover_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.medium.height\",\n            \"columnName\": \"anime_cover_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.large.width\",\n            \"columnName\": \"anime_cover_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.large.height\",\n            \"columnName\": \"anime_cover_meta_large_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.id\",\n            \"columnName\": \"manga_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.slug\",\n            \"columnName\": \"manga_slug\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.description\",\n            \"columnName\": \"manga_description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.titles\",\n            \"columnName\": \"manga_titles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.canonicalTitle\",\n            \"columnName\": \"manga_canonicalTitle\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.abbreviatedTitles\",\n            \"columnName\": \"manga_abbreviatedTitles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.averageRating\",\n            \"columnName\": \"manga_averageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.userCount\",\n            \"columnName\": \"manga_userCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.favoritesCount\",\n            \"columnName\": \"manga_favoritesCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.popularityRank\",\n            \"columnName\": \"manga_popularityRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingRank\",\n            \"columnName\": \"manga_ratingRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.startDate\",\n            \"columnName\": \"manga_startDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.endDate\",\n            \"columnName\": \"manga_endDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.nextRelease\",\n            \"columnName\": \"manga_nextRelease\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.tba\",\n            \"columnName\": \"manga_tba\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.status\",\n            \"columnName\": \"manga_status\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ageRating\",\n            \"columnName\": \"manga_ageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ageRatingGuide\",\n            \"columnName\": \"manga_ageRatingGuide\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.nsfw\",\n            \"columnName\": \"manga_nsfw\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.totalLength\",\n            \"columnName\": \"manga_totalLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.chapterCount\",\n            \"columnName\": \"manga_chapterCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.volumeCount\",\n            \"columnName\": \"manga_volumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.subtype\",\n            \"columnName\": \"manga_subtype\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.serialization\",\n            \"columnName\": \"manga_serialization\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r2\",\n            \"columnName\": \"manga_rating_r2\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r3\",\n            \"columnName\": \"manga_rating_r3\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r4\",\n            \"columnName\": \"manga_rating_r4\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r5\",\n            \"columnName\": \"manga_rating_r5\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r6\",\n            \"columnName\": \"manga_rating_r6\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r7\",\n            \"columnName\": \"manga_rating_r7\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r8\",\n            \"columnName\": \"manga_rating_r8\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r9\",\n            \"columnName\": \"manga_rating_r9\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r10\",\n            \"columnName\": \"manga_rating_r10\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r11\",\n            \"columnName\": \"manga_rating_r11\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r12\",\n            \"columnName\": \"manga_rating_r12\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r13\",\n            \"columnName\": \"manga_rating_r13\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r14\",\n            \"columnName\": \"manga_rating_r14\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r15\",\n            \"columnName\": \"manga_rating_r15\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r16\",\n            \"columnName\": \"manga_rating_r16\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r17\",\n            \"columnName\": \"manga_rating_r17\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r18\",\n            \"columnName\": \"manga_rating_r18\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r19\",\n            \"columnName\": \"manga_rating_r19\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r20\",\n            \"columnName\": \"manga_rating_r20\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.tiny\",\n            \"columnName\": \"manga_poster_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.small\",\n            \"columnName\": \"manga_poster_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.medium\",\n            \"columnName\": \"manga_poster_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.large\",\n            \"columnName\": \"manga_poster_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.original\",\n            \"columnName\": \"manga_poster_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"manga_poster_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"manga_poster_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.small.width\",\n            \"columnName\": \"manga_poster_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.small.height\",\n            \"columnName\": \"manga_poster_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.medium.width\",\n            \"columnName\": \"manga_poster_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.medium.height\",\n            \"columnName\": \"manga_poster_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.large.width\",\n            \"columnName\": \"manga_poster_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.large.height\",\n            \"columnName\": \"manga_poster_meta_large_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.tiny\",\n            \"columnName\": \"manga_cover_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.small\",\n            \"columnName\": \"manga_cover_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.medium\",\n            \"columnName\": \"manga_cover_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.large\",\n            \"columnName\": \"manga_cover_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.original\",\n            \"columnName\": \"manga_cover_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"manga_cover_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"manga_cover_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.small.width\",\n            \"columnName\": \"manga_cover_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.small.height\",\n            \"columnName\": \"manga_cover_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.medium.width\",\n            \"columnName\": \"manga_cover_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.medium.height\",\n            \"columnName\": \"manga_cover_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.large.width\",\n            \"columnName\": \"manga_cover_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.large.height\",\n            \"columnName\": \"manga_cover_meta_large_height\",\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        \"tableName\": \"library_entries_modifications\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `state` TEXT NOT NULL, `startedAt` TEXT, `finishedAt` TEXT, `status` INTEGER, `progress` INTEGER, `reconsumeCount` INTEGER, `volumesOwned` INTEGER, `ratingTwenty` INTEGER, `notes` TEXT, `privateEntry` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"state\",\n            \"columnName\": \"state\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"startedAt\",\n            \"columnName\": \"startedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"finishedAt\",\n            \"columnName\": \"finishedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progress\",\n            \"columnName\": \"progress\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsumeCount\",\n            \"columnName\": \"reconsumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"volumesOwned\",\n            \"columnName\": \"volumesOwned\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ratingTwenty\",\n            \"columnName\": \"ratingTwenty\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"notes\",\n            \"columnName\": \"notes\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"privateEntry\",\n            \"columnName\": \"privateEntry\",\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        \"tableName\": \"remote_keys\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`resourceId` TEXT NOT NULL COLLATE NOCASE, `remoteKeyType` TEXT NOT NULL, `prevPageKey` INTEGER, `nextPageKey` INTEGER, PRIMARY KEY(`resourceId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"resourceId\",\n            \"columnName\": \"resourceId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"remoteKeyType\",\n            \"columnName\": \"remoteKeyType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"prevPageKey\",\n            \"columnName\": \"prevPageKey\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"nextPageKey\",\n            \"columnName\": \"nextPageKey\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"resourceId\"\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, '9ffeea6c7e014697eeeb8de0dbec5a99')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.github.drumber.kitsune.data.source.local.LocalDatabase/2.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 2,\n    \"identityHash\": \"21e2d33c7212c6e12b735dc22bf563f5\",\n    \"entities\": [\n      {\n        \"tableName\": \"library_entries\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `updatedAt` TEXT, `startedAt` TEXT, `finishedAt` TEXT, `progressedAt` TEXT, `status` INTEGER, `progress` INTEGER, `reconsuming` INTEGER, `reconsumeCount` INTEGER, `volumesOwned` INTEGER, `ratingTwenty` INTEGER, `notes` TEXT, `privateEntry` INTEGER, `reactionSkipped` TEXT, `anime_id` TEXT, `anime_slug` TEXT, `anime_description` TEXT, `anime_titles` TEXT, `anime_canonicalTitle` TEXT, `anime_abbreviatedTitles` TEXT, `anime_averageRating` TEXT, `anime_userCount` INTEGER, `anime_favoritesCount` INTEGER, `anime_popularityRank` INTEGER, `anime_ratingRank` INTEGER, `anime_startDate` TEXT, `anime_endDate` TEXT, `anime_nextRelease` TEXT, `anime_tba` TEXT, `anime_status` TEXT, `anime_ageRating` TEXT, `anime_ageRatingGuide` TEXT, `anime_nsfw` INTEGER, `anime_totalLength` INTEGER, `anime_episodeCount` INTEGER, `anime_episodeLength` INTEGER, `anime_youtubeVideoId` TEXT, `anime_subtype` TEXT, `anime_rating_r2` TEXT, `anime_rating_r3` TEXT, `anime_rating_r4` TEXT, `anime_rating_r5` TEXT, `anime_rating_r6` TEXT, `anime_rating_r7` TEXT, `anime_rating_r8` TEXT, `anime_rating_r9` TEXT, `anime_rating_r10` TEXT, `anime_rating_r11` TEXT, `anime_rating_r12` TEXT, `anime_rating_r13` TEXT, `anime_rating_r14` TEXT, `anime_rating_r15` TEXT, `anime_rating_r16` TEXT, `anime_rating_r17` TEXT, `anime_rating_r18` TEXT, `anime_rating_r19` TEXT, `anime_rating_r20` TEXT, `anime_poster_tiny` TEXT, `anime_poster_small` TEXT, `anime_poster_medium` TEXT, `anime_poster_large` TEXT, `anime_poster_original` TEXT, `anime_poster_meta_tiny_width` INTEGER, `anime_poster_meta_tiny_height` INTEGER, `anime_poster_meta_small_width` INTEGER, `anime_poster_meta_small_height` INTEGER, `anime_poster_meta_medium_width` INTEGER, `anime_poster_meta_medium_height` INTEGER, `anime_poster_meta_large_width` INTEGER, `anime_poster_meta_large_height` INTEGER, `anime_cover_tiny` TEXT, `anime_cover_small` TEXT, `anime_cover_medium` TEXT, `anime_cover_large` TEXT, `anime_cover_original` TEXT, `anime_cover_meta_tiny_width` INTEGER, `anime_cover_meta_tiny_height` INTEGER, `anime_cover_meta_small_width` INTEGER, `anime_cover_meta_small_height` INTEGER, `anime_cover_meta_medium_width` INTEGER, `anime_cover_meta_medium_height` INTEGER, `anime_cover_meta_large_width` INTEGER, `anime_cover_meta_large_height` INTEGER, `manga_id` TEXT, `manga_slug` TEXT, `manga_description` TEXT, `manga_titles` TEXT, `manga_canonicalTitle` TEXT, `manga_abbreviatedTitles` TEXT, `manga_averageRating` TEXT, `manga_userCount` INTEGER, `manga_favoritesCount` INTEGER, `manga_popularityRank` INTEGER, `manga_ratingRank` INTEGER, `manga_startDate` TEXT, `manga_endDate` TEXT, `manga_nextRelease` TEXT, `manga_tba` TEXT, `manga_status` TEXT, `manga_ageRating` TEXT, `manga_ageRatingGuide` TEXT, `manga_nsfw` INTEGER, `manga_totalLength` INTEGER, `manga_chapterCount` INTEGER, `manga_volumeCount` INTEGER, `manga_subtype` TEXT, `manga_serialization` TEXT, `manga_rating_r2` TEXT, `manga_rating_r3` TEXT, `manga_rating_r4` TEXT, `manga_rating_r5` TEXT, `manga_rating_r6` TEXT, `manga_rating_r7` TEXT, `manga_rating_r8` TEXT, `manga_rating_r9` TEXT, `manga_rating_r10` TEXT, `manga_rating_r11` TEXT, `manga_rating_r12` TEXT, `manga_rating_r13` TEXT, `manga_rating_r14` TEXT, `manga_rating_r15` TEXT, `manga_rating_r16` TEXT, `manga_rating_r17` TEXT, `manga_rating_r18` TEXT, `manga_rating_r19` TEXT, `manga_rating_r20` TEXT, `manga_poster_tiny` TEXT, `manga_poster_small` TEXT, `manga_poster_medium` TEXT, `manga_poster_large` TEXT, `manga_poster_original` TEXT, `manga_poster_meta_tiny_width` INTEGER, `manga_poster_meta_tiny_height` INTEGER, `manga_poster_meta_small_width` INTEGER, `manga_poster_meta_small_height` INTEGER, `manga_poster_meta_medium_width` INTEGER, `manga_poster_meta_medium_height` INTEGER, `manga_poster_meta_large_width` INTEGER, `manga_poster_meta_large_height` INTEGER, `manga_cover_tiny` TEXT, `manga_cover_small` TEXT, `manga_cover_medium` TEXT, `manga_cover_large` TEXT, `manga_cover_original` TEXT, `manga_cover_meta_tiny_width` INTEGER, `manga_cover_meta_tiny_height` INTEGER, `manga_cover_meta_small_width` INTEGER, `manga_cover_meta_small_height` INTEGER, `manga_cover_meta_medium_width` INTEGER, `manga_cover_meta_medium_height` INTEGER, `manga_cover_meta_large_width` INTEGER, `manga_cover_meta_large_height` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updatedAt\",\n            \"columnName\": \"updatedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"startedAt\",\n            \"columnName\": \"startedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"finishedAt\",\n            \"columnName\": \"finishedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progressedAt\",\n            \"columnName\": \"progressedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progress\",\n            \"columnName\": \"progress\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsuming\",\n            \"columnName\": \"reconsuming\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsumeCount\",\n            \"columnName\": \"reconsumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"volumesOwned\",\n            \"columnName\": \"volumesOwned\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ratingTwenty\",\n            \"columnName\": \"ratingTwenty\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"notes\",\n            \"columnName\": \"notes\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"privateEntry\",\n            \"columnName\": \"privateEntry\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reactionSkipped\",\n            \"columnName\": \"reactionSkipped\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.id\",\n            \"columnName\": \"anime_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.slug\",\n            \"columnName\": \"anime_slug\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.description\",\n            \"columnName\": \"anime_description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.titles\",\n            \"columnName\": \"anime_titles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.canonicalTitle\",\n            \"columnName\": \"anime_canonicalTitle\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.abbreviatedTitles\",\n            \"columnName\": \"anime_abbreviatedTitles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.averageRating\",\n            \"columnName\": \"anime_averageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.userCount\",\n            \"columnName\": \"anime_userCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.favoritesCount\",\n            \"columnName\": \"anime_favoritesCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.popularityRank\",\n            \"columnName\": \"anime_popularityRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingRank\",\n            \"columnName\": \"anime_ratingRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.startDate\",\n            \"columnName\": \"anime_startDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.endDate\",\n            \"columnName\": \"anime_endDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.nextRelease\",\n            \"columnName\": \"anime_nextRelease\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.tba\",\n            \"columnName\": \"anime_tba\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.status\",\n            \"columnName\": \"anime_status\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ageRating\",\n            \"columnName\": \"anime_ageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ageRatingGuide\",\n            \"columnName\": \"anime_ageRatingGuide\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.nsfw\",\n            \"columnName\": \"anime_nsfw\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.totalLength\",\n            \"columnName\": \"anime_totalLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.episodeCount\",\n            \"columnName\": \"anime_episodeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.episodeLength\",\n            \"columnName\": \"anime_episodeLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.youtubeVideoId\",\n            \"columnName\": \"anime_youtubeVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.subtype\",\n            \"columnName\": \"anime_subtype\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r2\",\n            \"columnName\": \"anime_rating_r2\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r3\",\n            \"columnName\": \"anime_rating_r3\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r4\",\n            \"columnName\": \"anime_rating_r4\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r5\",\n            \"columnName\": \"anime_rating_r5\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r6\",\n            \"columnName\": \"anime_rating_r6\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r7\",\n            \"columnName\": \"anime_rating_r7\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r8\",\n            \"columnName\": \"anime_rating_r8\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r9\",\n            \"columnName\": \"anime_rating_r9\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r10\",\n            \"columnName\": \"anime_rating_r10\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r11\",\n            \"columnName\": \"anime_rating_r11\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r12\",\n            \"columnName\": \"anime_rating_r12\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r13\",\n            \"columnName\": \"anime_rating_r13\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r14\",\n            \"columnName\": \"anime_rating_r14\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r15\",\n            \"columnName\": \"anime_rating_r15\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r16\",\n            \"columnName\": \"anime_rating_r16\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r17\",\n            \"columnName\": \"anime_rating_r17\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r18\",\n            \"columnName\": \"anime_rating_r18\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r19\",\n            \"columnName\": \"anime_rating_r19\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.ratingFrequencies.r20\",\n            \"columnName\": \"anime_rating_r20\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.tiny\",\n            \"columnName\": \"anime_poster_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.small\",\n            \"columnName\": \"anime_poster_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.medium\",\n            \"columnName\": \"anime_poster_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.large\",\n            \"columnName\": \"anime_poster_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.original\",\n            \"columnName\": \"anime_poster_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"anime_poster_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"anime_poster_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.small.width\",\n            \"columnName\": \"anime_poster_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.small.height\",\n            \"columnName\": \"anime_poster_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.medium.width\",\n            \"columnName\": \"anime_poster_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.medium.height\",\n            \"columnName\": \"anime_poster_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.large.width\",\n            \"columnName\": \"anime_poster_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.posterImage.meta.dimensions.large.height\",\n            \"columnName\": \"anime_poster_meta_large_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.tiny\",\n            \"columnName\": \"anime_cover_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.small\",\n            \"columnName\": \"anime_cover_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.medium\",\n            \"columnName\": \"anime_cover_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.large\",\n            \"columnName\": \"anime_cover_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.original\",\n            \"columnName\": \"anime_cover_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"anime_cover_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"anime_cover_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.small.width\",\n            \"columnName\": \"anime_cover_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.small.height\",\n            \"columnName\": \"anime_cover_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.medium.width\",\n            \"columnName\": \"anime_cover_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.medium.height\",\n            \"columnName\": \"anime_cover_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.large.width\",\n            \"columnName\": \"anime_cover_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"anime.coverImage.meta.dimensions.large.height\",\n            \"columnName\": \"anime_cover_meta_large_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.id\",\n            \"columnName\": \"manga_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.slug\",\n            \"columnName\": \"manga_slug\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.description\",\n            \"columnName\": \"manga_description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.titles\",\n            \"columnName\": \"manga_titles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.canonicalTitle\",\n            \"columnName\": \"manga_canonicalTitle\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.abbreviatedTitles\",\n            \"columnName\": \"manga_abbreviatedTitles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.averageRating\",\n            \"columnName\": \"manga_averageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.userCount\",\n            \"columnName\": \"manga_userCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.favoritesCount\",\n            \"columnName\": \"manga_favoritesCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.popularityRank\",\n            \"columnName\": \"manga_popularityRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingRank\",\n            \"columnName\": \"manga_ratingRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.startDate\",\n            \"columnName\": \"manga_startDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.endDate\",\n            \"columnName\": \"manga_endDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.nextRelease\",\n            \"columnName\": \"manga_nextRelease\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.tba\",\n            \"columnName\": \"manga_tba\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.status\",\n            \"columnName\": \"manga_status\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ageRating\",\n            \"columnName\": \"manga_ageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ageRatingGuide\",\n            \"columnName\": \"manga_ageRatingGuide\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.nsfw\",\n            \"columnName\": \"manga_nsfw\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.totalLength\",\n            \"columnName\": \"manga_totalLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.chapterCount\",\n            \"columnName\": \"manga_chapterCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.volumeCount\",\n            \"columnName\": \"manga_volumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.subtype\",\n            \"columnName\": \"manga_subtype\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.serialization\",\n            \"columnName\": \"manga_serialization\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r2\",\n            \"columnName\": \"manga_rating_r2\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r3\",\n            \"columnName\": \"manga_rating_r3\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r4\",\n            \"columnName\": \"manga_rating_r4\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r5\",\n            \"columnName\": \"manga_rating_r5\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r6\",\n            \"columnName\": \"manga_rating_r6\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r7\",\n            \"columnName\": \"manga_rating_r7\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r8\",\n            \"columnName\": \"manga_rating_r8\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r9\",\n            \"columnName\": \"manga_rating_r9\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r10\",\n            \"columnName\": \"manga_rating_r10\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r11\",\n            \"columnName\": \"manga_rating_r11\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r12\",\n            \"columnName\": \"manga_rating_r12\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r13\",\n            \"columnName\": \"manga_rating_r13\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r14\",\n            \"columnName\": \"manga_rating_r14\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r15\",\n            \"columnName\": \"manga_rating_r15\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r16\",\n            \"columnName\": \"manga_rating_r16\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r17\",\n            \"columnName\": \"manga_rating_r17\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r18\",\n            \"columnName\": \"manga_rating_r18\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r19\",\n            \"columnName\": \"manga_rating_r19\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.ratingFrequencies.r20\",\n            \"columnName\": \"manga_rating_r20\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.tiny\",\n            \"columnName\": \"manga_poster_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.small\",\n            \"columnName\": \"manga_poster_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.medium\",\n            \"columnName\": \"manga_poster_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.large\",\n            \"columnName\": \"manga_poster_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.original\",\n            \"columnName\": \"manga_poster_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"manga_poster_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"manga_poster_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.small.width\",\n            \"columnName\": \"manga_poster_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.small.height\",\n            \"columnName\": \"manga_poster_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.medium.width\",\n            \"columnName\": \"manga_poster_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.medium.height\",\n            \"columnName\": \"manga_poster_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.large.width\",\n            \"columnName\": \"manga_poster_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.posterImage.meta.dimensions.large.height\",\n            \"columnName\": \"manga_poster_meta_large_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.tiny\",\n            \"columnName\": \"manga_cover_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.small\",\n            \"columnName\": \"manga_cover_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.medium\",\n            \"columnName\": \"manga_cover_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.large\",\n            \"columnName\": \"manga_cover_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.original\",\n            \"columnName\": \"manga_cover_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"manga_cover_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"manga_cover_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.small.width\",\n            \"columnName\": \"manga_cover_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.small.height\",\n            \"columnName\": \"manga_cover_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.medium.width\",\n            \"columnName\": \"manga_cover_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.medium.height\",\n            \"columnName\": \"manga_cover_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.large.width\",\n            \"columnName\": \"manga_cover_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"manga.coverImage.meta.dimensions.large.height\",\n            \"columnName\": \"manga_cover_meta_large_height\",\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        \"tableName\": \"library_entries_modifications\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `createTime` INTEGER NOT NULL, `state` TEXT NOT NULL, `startedAt` TEXT, `finishedAt` TEXT, `status` INTEGER, `progress` INTEGER, `reconsumeCount` INTEGER, `volumesOwned` INTEGER, `ratingTwenty` INTEGER, `notes` TEXT, `privateEntry` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createTime\",\n            \"columnName\": \"createTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"state\",\n            \"columnName\": \"state\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"startedAt\",\n            \"columnName\": \"startedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"finishedAt\",\n            \"columnName\": \"finishedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progress\",\n            \"columnName\": \"progress\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsumeCount\",\n            \"columnName\": \"reconsumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"volumesOwned\",\n            \"columnName\": \"volumesOwned\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ratingTwenty\",\n            \"columnName\": \"ratingTwenty\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"notes\",\n            \"columnName\": \"notes\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"privateEntry\",\n            \"columnName\": \"privateEntry\",\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        \"tableName\": \"remote_keys\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`resourceId` TEXT NOT NULL COLLATE NOCASE, `remoteKeyType` TEXT NOT NULL, `prevPageKey` INTEGER, `nextPageKey` INTEGER, PRIMARY KEY(`resourceId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"resourceId\",\n            \"columnName\": \"resourceId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"remoteKeyType\",\n            \"columnName\": \"remoteKeyType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"prevPageKey\",\n            \"columnName\": \"prevPageKey\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"nextPageKey\",\n            \"columnName\": \"nextPageKey\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"resourceId\"\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, '21e2d33c7212c6e12b735dc22bf563f5')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.github.drumber.kitsune.data.source.local.LocalDatabase/3.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 3,\n    \"identityHash\": \"7979a798f0004298608e29c0014d940d\",\n    \"entities\": [\n      {\n        \"tableName\": \"library_entries\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `updatedAt` TEXT, `startedAt` TEXT, `finishedAt` TEXT, `progressedAt` TEXT, `status` INTEGER, `progress` INTEGER, `reconsuming` INTEGER, `reconsumeCount` INTEGER, `volumesOwned` INTEGER, `ratingTwenty` INTEGER, `notes` TEXT, `privateEntry` INTEGER, `reactionSkipped` TEXT, `media_id` TEXT, `media_type` TEXT, `media_description` TEXT, `media_titles` TEXT, `media_canonicalTitle` TEXT, `media_abbreviatedTitles` TEXT, `media_averageRating` TEXT, `media_popularityRank` INTEGER, `media_ratingRank` INTEGER, `media_startDate` TEXT, `media_endDate` TEXT, `media_nextRelease` TEXT, `media_tba` TEXT, `media_status` TEXT, `media_ageRating` TEXT, `media_ageRatingGuide` TEXT, `media_nsfw` INTEGER, `media_animeSubtype` TEXT, `media_totalLength` INTEGER, `media_episodeCount` INTEGER, `media_episodeLength` INTEGER, `media_mangaSubtype` TEXT, `media_chapterCount` INTEGER, `media_volumeCount` INTEGER, `media_serialization` TEXT, `media_rating_r2` TEXT, `media_rating_r3` TEXT, `media_rating_r4` TEXT, `media_rating_r5` TEXT, `media_rating_r6` TEXT, `media_rating_r7` TEXT, `media_rating_r8` TEXT, `media_rating_r9` TEXT, `media_rating_r10` TEXT, `media_rating_r11` TEXT, `media_rating_r12` TEXT, `media_rating_r13` TEXT, `media_rating_r14` TEXT, `media_rating_r15` TEXT, `media_rating_r16` TEXT, `media_rating_r17` TEXT, `media_rating_r18` TEXT, `media_rating_r19` TEXT, `media_rating_r20` TEXT, `media_poster_tiny` TEXT, `media_poster_small` TEXT, `media_poster_medium` TEXT, `media_poster_large` TEXT, `media_poster_original` TEXT, `media_poster_meta_tiny_width` INTEGER, `media_poster_meta_tiny_height` INTEGER, `media_poster_meta_small_width` INTEGER, `media_poster_meta_small_height` INTEGER, `media_poster_meta_medium_width` INTEGER, `media_poster_meta_medium_height` INTEGER, `media_poster_meta_large_width` INTEGER, `media_poster_meta_large_height` INTEGER, `media_cover_tiny` TEXT, `media_cover_small` TEXT, `media_cover_medium` TEXT, `media_cover_large` TEXT, `media_cover_original` TEXT, `media_cover_meta_tiny_width` INTEGER, `media_cover_meta_tiny_height` INTEGER, `media_cover_meta_small_width` INTEGER, `media_cover_meta_small_height` INTEGER, `media_cover_meta_medium_width` INTEGER, `media_cover_meta_medium_height` INTEGER, `media_cover_meta_large_width` INTEGER, `media_cover_meta_large_height` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updatedAt\",\n            \"columnName\": \"updatedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"startedAt\",\n            \"columnName\": \"startedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"finishedAt\",\n            \"columnName\": \"finishedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progressedAt\",\n            \"columnName\": \"progressedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progress\",\n            \"columnName\": \"progress\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsuming\",\n            \"columnName\": \"reconsuming\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsumeCount\",\n            \"columnName\": \"reconsumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"volumesOwned\",\n            \"columnName\": \"volumesOwned\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ratingTwenty\",\n            \"columnName\": \"ratingTwenty\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"notes\",\n            \"columnName\": \"notes\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"privateEntry\",\n            \"columnName\": \"privateEntry\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reactionSkipped\",\n            \"columnName\": \"reactionSkipped\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.id\",\n            \"columnName\": \"media_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.type\",\n            \"columnName\": \"media_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.description\",\n            \"columnName\": \"media_description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.titles\",\n            \"columnName\": \"media_titles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.canonicalTitle\",\n            \"columnName\": \"media_canonicalTitle\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.abbreviatedTitles\",\n            \"columnName\": \"media_abbreviatedTitles\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.averageRating\",\n            \"columnName\": \"media_averageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.popularityRank\",\n            \"columnName\": \"media_popularityRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingRank\",\n            \"columnName\": \"media_ratingRank\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.startDate\",\n            \"columnName\": \"media_startDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.endDate\",\n            \"columnName\": \"media_endDate\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.nextRelease\",\n            \"columnName\": \"media_nextRelease\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.tba\",\n            \"columnName\": \"media_tba\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.status\",\n            \"columnName\": \"media_status\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ageRating\",\n            \"columnName\": \"media_ageRating\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ageRatingGuide\",\n            \"columnName\": \"media_ageRatingGuide\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.nsfw\",\n            \"columnName\": \"media_nsfw\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.animeSubtype\",\n            \"columnName\": \"media_animeSubtype\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.totalLength\",\n            \"columnName\": \"media_totalLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.episodeCount\",\n            \"columnName\": \"media_episodeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.episodeLength\",\n            \"columnName\": \"media_episodeLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.mangaSubtype\",\n            \"columnName\": \"media_mangaSubtype\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.chapterCount\",\n            \"columnName\": \"media_chapterCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.volumeCount\",\n            \"columnName\": \"media_volumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.serialization\",\n            \"columnName\": \"media_serialization\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r2\",\n            \"columnName\": \"media_rating_r2\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r3\",\n            \"columnName\": \"media_rating_r3\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r4\",\n            \"columnName\": \"media_rating_r4\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r5\",\n            \"columnName\": \"media_rating_r5\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r6\",\n            \"columnName\": \"media_rating_r6\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r7\",\n            \"columnName\": \"media_rating_r7\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r8\",\n            \"columnName\": \"media_rating_r8\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r9\",\n            \"columnName\": \"media_rating_r9\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r10\",\n            \"columnName\": \"media_rating_r10\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r11\",\n            \"columnName\": \"media_rating_r11\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r12\",\n            \"columnName\": \"media_rating_r12\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r13\",\n            \"columnName\": \"media_rating_r13\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r14\",\n            \"columnName\": \"media_rating_r14\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r15\",\n            \"columnName\": \"media_rating_r15\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r16\",\n            \"columnName\": \"media_rating_r16\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r17\",\n            \"columnName\": \"media_rating_r17\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r18\",\n            \"columnName\": \"media_rating_r18\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r19\",\n            \"columnName\": \"media_rating_r19\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.ratingFrequencies.r20\",\n            \"columnName\": \"media_rating_r20\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.tiny\",\n            \"columnName\": \"media_poster_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.small\",\n            \"columnName\": \"media_poster_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.medium\",\n            \"columnName\": \"media_poster_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.large\",\n            \"columnName\": \"media_poster_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.original\",\n            \"columnName\": \"media_poster_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"media_poster_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"media_poster_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.meta.dimensions.small.width\",\n            \"columnName\": \"media_poster_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.meta.dimensions.small.height\",\n            \"columnName\": \"media_poster_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.meta.dimensions.medium.width\",\n            \"columnName\": \"media_poster_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.meta.dimensions.medium.height\",\n            \"columnName\": \"media_poster_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.meta.dimensions.large.width\",\n            \"columnName\": \"media_poster_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.posterImage.meta.dimensions.large.height\",\n            \"columnName\": \"media_poster_meta_large_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.tiny\",\n            \"columnName\": \"media_cover_tiny\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.small\",\n            \"columnName\": \"media_cover_small\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.medium\",\n            \"columnName\": \"media_cover_medium\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.large\",\n            \"columnName\": \"media_cover_large\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.original\",\n            \"columnName\": \"media_cover_original\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.meta.dimensions.tiny.width\",\n            \"columnName\": \"media_cover_meta_tiny_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.meta.dimensions.tiny.height\",\n            \"columnName\": \"media_cover_meta_tiny_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.meta.dimensions.small.width\",\n            \"columnName\": \"media_cover_meta_small_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.meta.dimensions.small.height\",\n            \"columnName\": \"media_cover_meta_small_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.meta.dimensions.medium.width\",\n            \"columnName\": \"media_cover_meta_medium_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.meta.dimensions.medium.height\",\n            \"columnName\": \"media_cover_meta_medium_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.meta.dimensions.large.width\",\n            \"columnName\": \"media_cover_meta_large_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"media.coverImage.meta.dimensions.large.height\",\n            \"columnName\": \"media_cover_meta_large_height\",\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        \"tableName\": \"library_entries_modifications\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `createTime` INTEGER NOT NULL, `state` TEXT NOT NULL, `startedAt` TEXT, `finishedAt` TEXT, `status` INTEGER, `progress` INTEGER, `reconsumeCount` INTEGER, `volumesOwned` INTEGER, `ratingTwenty` INTEGER, `notes` TEXT, `privateEntry` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createTime\",\n            \"columnName\": \"createTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"state\",\n            \"columnName\": \"state\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"startedAt\",\n            \"columnName\": \"startedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"finishedAt\",\n            \"columnName\": \"finishedAt\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"progress\",\n            \"columnName\": \"progress\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"reconsumeCount\",\n            \"columnName\": \"reconsumeCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"volumesOwned\",\n            \"columnName\": \"volumesOwned\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ratingTwenty\",\n            \"columnName\": \"ratingTwenty\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"notes\",\n            \"columnName\": \"notes\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"privateEntry\",\n            \"columnName\": \"privateEntry\",\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        \"tableName\": \"remote_keys\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`resourceId` TEXT NOT NULL COLLATE NOCASE, `remoteKeyType` TEXT NOT NULL, `prevPageKey` INTEGER, `nextPageKey` INTEGER, PRIMARY KEY(`resourceId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"resourceId\",\n            \"columnName\": \"resourceId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"remoteKeyType\",\n            \"columnName\": \"remoteKeyType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"prevPageKey\",\n            \"columnName\": \"prevPageKey\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"nextPageKey\",\n            \"columnName\": \"nextPageKey\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"resourceId\"\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, '7979a798f0004298608e29c0014d940d')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/src/androidTest/java/io/github/drumber/kitsune/fastlane/CaptureScreenshots.kt",
    "content": "package io.github.drumber.kitsune.fastlane\n\nimport android.net.Uri\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.navigation.findNavController\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.Espresso.pressBack\nimport androidx.test.espresso.IdlingRegistry\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport androidx.test.espresso.action.ViewActions.swipeUp\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.ext.junit.rules.ActivityScenarioRule\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.internal.runner.junit4.statement.UiThreadStatement\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.rule.GrantPermissionRule\nimport io.github.drumber.kitsune.BuildConfig\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.AppTheme\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.main.MainActivity\nimport io.github.drumber.kitsune.utils.OkHttpIdlingResource\nimport io.github.drumber.kitsune.utils.filter.RequiresScreenshotMode\nimport io.github.drumber.kitsune.utils.waitForView\nimport okhttp3.OkHttpClient\nimport org.junit.AfterClass\nimport org.junit.Assume.assumeTrue\nimport org.junit.BeforeClass\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.get\nimport org.koin.core.qualifier.named\nimport tools.fastlane.screengrab.Screengrab\nimport tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar\nimport kotlin.time.Duration.Companion.seconds\n\n@RunWith(AndroidJUnit4::class)\nclass CaptureScreenshots : KoinComponent {\n\n    @get:Rule\n    var activityRule = ActivityScenarioRule(MainActivity::class.java)\n\n    @get:Rule\n    var runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(\n        android.Manifest.permission.POST_NOTIFICATIONS,\n        android.Manifest.permission.DUMP\n    )\n\n    companion object {\n        @BeforeClass\n        @JvmStatic\n        fun beforeAll() {\n            assumeTrue(BuildConfig.SCREENSHOT_MODE_ENABLED)\n        }\n\n        @AfterClass\n        @JvmStatic\n        fun afterAll() {\n            CleanStatusBar.disable()\n        }\n\n        fun enterDemoMode() {\n            CleanStatusBar()\n                .setNetworkFullyConnected(true)\n                .setShowNotifications(false)\n                .setMobileNetworkLevel(4)\n                .setClock(\"1200\")\n                .enable()\n        }\n    }\n\n    @RequiresScreenshotMode\n    @Test\n    fun testTakeScreenshot() {\n        enterDemoMode()\n\n        val idlingResource = mutableListOf<OkHttpIdlingResource>()\n        activityRule.scenario.onActivity {\n            val client: OkHttpClient = get()\n            val imageClient: OkHttpClient = get(named(\"images\"))\n            idlingResource.add(OkHttpIdlingResource(client))\n            idlingResource.add(OkHttpIdlingResource(imageClient))\n        }\n        IdlingRegistry.getInstance().register(*idlingResource.toTypedArray())\n\n        // Light Mode\n        KitsunePref.darkMode = AppCompatDelegate.MODE_NIGHT_NO.toString()\n        takeHomeScreenshots(\"light\")\n        takeSearchScreenshots(\"light\")\n        takeDetailsScreenshot(\"light\")\n\n        // Dark Mode\n        UiThreadStatement.runOnUiThread {\n            KitsunePref.darkMode = AppCompatDelegate.MODE_NIGHT_YES.toString()\n            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)\n        }\n        takeHomeScreenshots(\"dark\")\n        takeSearchScreenshots(\"dark\")\n        takeDetailsScreenshot(\"dark\")\n\n        // Purple theme with Dark Mode\n        UiThreadStatement.runOnUiThread {\n            KitsunePref.appTheme = AppTheme.PURPLE\n        }\n        takeHomeScreenshots(\"dark_purple\")\n\n        // Purple theme with Light Mode\n        UiThreadStatement.runOnUiThread {\n            KitsunePref.darkMode = AppCompatDelegate.MODE_NIGHT_NO.toString()\n            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)\n        }\n        takeHomeScreenshots(\"light_purple\")\n\n        IdlingRegistry.getInstance().unregister(*idlingResource.toTypedArray())\n        idlingResource.clear()\n    }\n\n    private fun takeHomeScreenshots(prefix: String) {\n        onView(withId(R.id.main_fragment)).perform(click())\n\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n        Thread.sleep(1000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        Screengrab.screenshot(\"${prefix}_home_screen\")\n    }\n\n    private fun takeSearchScreenshots(prefix: String) {\n        onView(withId(R.id.search_fragment)).perform(click())\n\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n        Thread.sleep(3000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        Screengrab.screenshot(\"${prefix}_search_screen\")\n    }\n\n    private fun takeDetailsScreenshot(prefix: String) {\n        activityRule.scenario.onActivity { activity ->\n            val navController = activity.findNavController(R.id.nav_host_fragment)\n            navController.navigate(Uri.parse(\"${Kitsu.BASE_URL}/anime/12\"))\n        }\n\n        onView(isRoot()).perform(waitForView(R.id.tv_description, 30.seconds))\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n        Thread.sleep(3000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        Screengrab.screenshot(\"${prefix}_details_screen\")\n\n        Thread.sleep(3000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        onView(withId(R.id.layout_ratings)).perform(scrollTo())\n        onView(withId(R.id.nsv_content)).perform(swipeUp())\n        Thread.sleep(100) // wait for scroll\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        Screengrab.screenshot(\"${prefix}_details_ratings_screen\")\n        pressBack()\n    }\n}"
  },
  {
    "path": "app/src/androidTest/java/io/github/drumber/kitsune/navigation/NavigationTest.kt",
    "content": "package io.github.drumber.kitsune.navigation\n\nimport android.net.Uri\nimport androidx.navigation.findNavController\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.IdlingRegistry\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.pressBack\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport androidx.test.espresso.action.ViewActions.typeText\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.isNotEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.espresso.matcher.ViewMatchers.withChild\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.rules.ActivityScenarioRule\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.rule.GrantPermissionRule\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.ui.adapter.MediaViewHolder\nimport io.github.drumber.kitsune.ui.adapter.paging.CharacterPagingAdapter.CharacterViewHolder\nimport io.github.drumber.kitsune.ui.adapter.paging.MediaUnitPagingAdapter.MediaUnitViewHolder\nimport io.github.drumber.kitsune.ui.main.MainActivity\nimport io.github.drumber.kitsune.utils.OkHttpIdlingResource\nimport io.github.drumber.kitsune.utils.actionOnChild\nimport io.github.drumber.kitsune.utils.searchText\nimport io.github.drumber.kitsune.utils.waitForView\nimport okhttp3.OkHttpClient\nimport org.hamcrest.core.AllOf.allOf\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.get\nimport kotlin.time.Duration.Companion.seconds\n\n@RunWith(AndroidJUnit4::class)\nclass NavigationTest : KoinComponent {\n\n    @get:Rule\n    var activityRule = ActivityScenarioRule(MainActivity::class.java)\n\n    @get:Rule\n    var runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(\n        android.Manifest.permission.POST_NOTIFICATIONS\n    )\n\n    private var idlingResource: OkHttpIdlingResource? = null\n\n    @Before\n    fun setup() {\n        activityRule.scenario.onActivity {\n            val client: OkHttpClient = get()\n            idlingResource = OkHttpIdlingResource(client)\n        }\n        IdlingRegistry.getInstance().register(idlingResource!!)\n    }\n\n    @After\n    fun tearDown() {\n        IdlingRegistry.getInstance().unregister(idlingResource)\n    }\n\n    @Test\n    fun shouldNavigateToDestinationsFromHome() {\n        onView(withId(R.id.main_fragment)).perform(click())\n\n        Thread.sleep(1000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // navigate to trending section\n        onView(\n            allOf(\n                withChild(withText(R.string.section_trending)),\n                withId(R.id.header)\n            )\n        ).perform(click())\n\n        Thread.sleep(1000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // click first media item\n        onView(withId(R.id.rv_media)).perform(actionOnItemAtPosition<MediaViewHolder>(0, click()))\n    }\n\n    @Test\n    fun shouldNavigateToSearchFragment() {\n        onView(withId(R.id.search_fragment)).perform(click())\n\n        Thread.sleep(3000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // perform search\n        onView(withId(R.id.search_view)).perform(searchText(\"toradora\"))\n        Thread.sleep(1000)\n\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n        onView(isRoot()).perform(waitForView(R.id.rv_media, 10.seconds))\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // click first media item\n        onView(withId(R.id.rv_media)).perform(actionOnItemAtPosition<MediaViewHolder>(0, click()))\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // go back to search fragment\n        onView(isRoot()).perform(pressBack())\n\n        // open filters\n        onView(withId(R.id.btn_filter)).perform(click())\n        Thread.sleep(1000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // open categories\n        onView(withId(R.id.card_categories)).perform(scrollTo(), click())\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // go back to search fragment\n        onView(isRoot()).perform(pressBack())\n        onView(isRoot()).perform(pressBack())\n    }\n\n    @Test\n    fun shouldNavigateToLibraryFragment() {\n        onView(withId(R.id.library_fragment)).perform(click())\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n    }\n\n    @Test\n    fun shouldNavigateToProfileFragmentAndSettings() {\n        onView(withId(R.id.profile_fragment)).perform(click())\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // open settings\n        onView(withId(R.id.menu_settings)).perform(click())\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // navigate to appearance\n        onView(withText(R.string.nav_appearance)).perform(click())\n    }\n\n    @Test\n    fun shouldNavigateToDetailsAndSubPages() {\n        activityRule.scenario.onActivity { activity ->\n            val navController = activity.findNavController(R.id.nav_host_fragment)\n            navController.navigate(Uri.parse(\"${Kitsu.BASE_URL}/anime/12\"))\n        }\n\n        onView(isRoot()).perform(waitForView(R.id.tv_description, 30.seconds))\n        Thread.sleep(3000)\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n        Thread.sleep(3000)\n\n        fun goBack() {\n            onView(isRoot()).perform(pressBack())\n            Thread.sleep(500)\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n            onView(isRoot()).perform(pressBack())\n        }\n\n        fun navigateToEpisodes() {\n            // navigate to episodes\n            onView(withId(R.id.btn_media_units)).perform(scrollTo())\n            Thread.sleep(100)\n            onView(withId(R.id.btn_media_units)).perform(click())\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n            // click on first episode\n            onView(withId(R.id.rv_media)).perform(\n                actionOnItemAtPosition<MediaUnitViewHolder>(\n                    0,\n                    click()\n                )\n            )\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n            // go back to details fragment\n            goBack()\n        }\n\n        fun navigateToCharacters() {\n            // navigate to characters\n            onView(withId(R.id.btn_characters)).perform(scrollTo())\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n            onView(withId(R.id.btn_characters)).perform(click())\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n            // click on first character\n            onView(withId(R.id.rv_media)).perform(\n                actionOnItemAtPosition<CharacterViewHolder>(\n                    1,\n                    actionOnChild(withId(R.id.iv_character), click())\n                )\n            )\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n            // go back to details fragment\n            goBack()\n        }\n\n        fun navigateToCategory() {\n            // navigate to category\n            onView(withChild(withId(R.id.chip_group_categories))).perform(scrollTo())\n            onView(withId(R.id.chip_group_categories)).perform(click())\n\n            Thread.sleep(1000)\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n            // click first media item\n            onView(withId(R.id.rv_media)).perform(actionOnItemAtPosition<MediaViewHolder>(0, click()))\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n            // go back to details fragment\n            goBack()\n        }\n\n        navigateToEpisodes()\n        navigateToCharacters()\n        navigateToCategory()\n    }\n\n    @Test\n    fun shouldNavigateToLoginScreen() {\n        // navigate to profile fragment\n        onView(withId(R.id.profile_fragment)).perform(click())\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        // navigate to login screen\n        onView(withId(R.id.btn_login)).perform(click())\n\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n\n        onView(withId(R.id.btn_login)).check(matches(isNotEnabled()))\n\n        onView(withId(R.id.input_username)).perform(typeText(\"user@example.com\"))\n        onView(withId(R.id.btn_login)).check(matches(isNotEnabled()))\n\n        onView(withId(R.id.input_password)).perform(typeText(\"password\"))\n\n        onView(withId(R.id.btn_login)).check(matches(isEnabled()))\n\n        // go back to profile fragment\n        onView(isRoot()).perform(pressBack())\n        InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n    }\n}"
  },
  {
    "path": "app/src/androidTest/java/io/github/drumber/kitsune/utils/OkHttpIdlingResource.kt",
    "content": "package io.github.drumber.kitsune.utils\n\nimport androidx.test.espresso.IdlingResource\nimport okhttp3.Dispatcher\nimport okhttp3.OkHttpClient\n\nclass OkHttpIdlingResource(okHttpClient: OkHttpClient) : IdlingResource {\n\n    @Volatile\n    private var callback: IdlingResource.ResourceCallback? = null\n\n    private val dispatcher: Dispatcher\n\n    init {\n        dispatcher = okHttpClient.dispatcher\n        okHttpClient.dispatcher.idleCallback = Runnable { callback?.onTransitionToIdle() }\n    }\n\n    override fun getName(): String {\n        return OkHttpIdlingResource::class.java.name\n    }\n\n    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {\n        this.callback = callback\n    }\n\n    override fun isIdleNow(): Boolean {\n        return dispatcher.runningCallsCount() == 0\n    }\n}"
  },
  {
    "path": "app/src/androidTest/java/io/github/drumber/kitsune/utils/SearchViewActions.kt",
    "content": "package io.github.drumber.kitsune.utils\n\nimport android.view.View\nimport androidx.appcompat.widget.SearchView\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport org.hamcrest.Matcher\nimport org.hamcrest.core.AllOf.allOf\n\nfun searchText(query: String) = object : ViewAction {\n    override fun getConstraints(): Matcher<View> {\n        return allOf(\n            isDisplayed(),\n            isAssignableFrom(SearchView::class.java)\n        )\n    }\n\n    override fun getDescription() = \"Type text into a SearchView\"\n\n    override fun perform(uiController: androidx.test.espresso.UiController, view: View) {\n        val searchView = view as SearchView\n        searchView.setQuery(query, true)\n    }\n}"
  },
  {
    "path": "app/src/androidTest/java/io/github/drumber/kitsune/utils/ViewActions.kt",
    "content": "package io.github.drumber.kitsune.utils\n\nimport android.view.View\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.util.TreeIterables\nimport org.hamcrest.Matcher\nimport org.hamcrest.StringDescription\nimport org.hamcrest.core.AllOf.allOf\n\nfun actionOnChild(matcher: Matcher<View>, action: ViewAction) = object : ViewAction {\n    override fun getConstraints(): Matcher<View> {\n        return allOf(isDisplayed(), matcher)\n    }\n\n    override fun getDescription(): String = \"Performing action on child\"\n\n    override fun perform(uiController: UiController, view: View) {\n        val results = TreeIterables.breadthFirstViewTraversal(view).filter { matcher.matches(it) }\n\n        if (results.isEmpty()) {\n            throw RuntimeException(\"No view found with matcher ${StringDescription.asString(matcher)} in the hierarchy of $view\")\n        } else if (results.size > 1) {\n            throw RuntimeException(\"Multiple views found with matcher ${StringDescription.asString(matcher)} in the hierarchy of $view\")\n        }\n\n        action.perform(uiController, results.first())\n    }\n}"
  },
  {
    "path": "app/src/androidTest/java/io/github/drumber/kitsune/utils/WaitForView.kt",
    "content": "package io.github.drumber.kitsune.utils\n\nimport android.view.View\nimport androidx.annotation.IdRes\nimport androidx.test.espresso.PerformException\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.util.HumanReadables\nimport androidx.test.espresso.util.TreeIterables\nimport org.hamcrest.Matcher\nimport java.util.concurrent.TimeoutException\nimport kotlin.time.Duration\n\nclass WaitForView(\n    @IdRes private val viewId: Int,\n    private val timeout: Duration\n) : ViewAction {\n\n    override fun getDescription(): String {\n        return \"wait up to ${timeout.inWholeMilliseconds} milliseconds to find a view with ID $viewId\"\n    }\n\n    override fun getConstraints(): Matcher<View> {\n        return isRoot()\n    }\n\n    override fun perform(uiController: UiController, view: View) {\n        uiController.loopMainThreadUntilIdle()\n\n        val endTime = System.currentTimeMillis() + timeout.inWholeMilliseconds\n\n        do {\n            for (child in TreeIterables.breadthFirstViewTraversal(view)) {\n                if (withId(viewId).matches(child))\n                    return\n            }\n            uiController.loopMainThreadForAtLeast(500)\n        } while (System.currentTimeMillis() < endTime)\n\n        throw PerformException.Builder()\n            .withActionDescription(description)\n            .withCause(TimeoutException(\"Waited $timeout milliseconds\"))\n            .withViewDescription(HumanReadables.describe(view))\n            .build()\n    }\n}\n\nfun waitForView(@IdRes viewId: Int, timeout: Duration) = WaitForView(viewId, timeout)\n"
  },
  {
    "path": "app/src/androidTest/java/io/github/drumber/kitsune/utils/filter/RequiresScreenshotMode.kt",
    "content": "package io.github.drumber.kitsune.utils.filter\n\nimport androidx.test.filters.CustomFilter\n\n@Retention(AnnotationRetention.RUNTIME)\n@Target(AnnotationTarget.FUNCTION)\n@CustomFilter(filterClass = ScreenshotModeCustomFilter::class)\nannotation class RequiresScreenshotMode\n"
  },
  {
    "path": "app/src/androidTest/java/io/github/drumber/kitsune/utils/filter/ScreenshotModeCustomFilter.kt",
    "content": "package io.github.drumber.kitsune.utils.filter\n\nimport androidx.test.filters.AbstractFilter\nimport io.github.drumber.kitsune.BuildConfig\nimport org.junit.runner.Description\n\nclass ScreenshotModeCustomFilter : AbstractFilter() {\n\n    override fun describe(): String {\n        return \"only run test if BuildConfig property 'SCREENSHOT_MODE_ENABLED' is 'true'\"\n    }\n\n    override fun evaluateTest(description: Description?): Boolean {\n       return BuildConfig.SCREENSHOT_MODE_ENABLED\n    }\n}"
  },
  {
    "path": "app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <!-- Allows storing screenshots on external storage, where it can be accessed by ADB -->\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"28\" />\n\n    <!-- Allows changing locales -->\n    <uses-permission\n        android:name=\"android.permission.CHANGE_CONFIGURATION\"\n        tools:ignore=\"ProtectedPermissions\" />\n\n    <!-- Allows changing SystemUI demo mode -->\n    <uses-permission\n        android:name=\"android.permission.DUMP\"\n        tools:ignore=\"ProtectedPermissions\" />\n\n</manifest>"
  },
  {
    "path": "app/src/instrumented/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <!-- Allows storing screenshots on external storage, where it can be accessed by ADB -->\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"28\" />\n\n    <!-- Allows changing locales -->\n    <uses-permission\n        android:name=\"android.permission.CHANGE_CONFIGURATION\"\n        tools:ignore=\"ProtectedPermissions\" />\n\n    <!-- Allows changing SystemUI demo mode -->\n    <uses-permission\n        android:name=\"android.permission.DUMP\"\n        tools:ignore=\"ProtectedPermissions\" />\n\n</manifest>"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\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.POST_NOTIFICATIONS\" />\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"28\" />\n\n    <application\n        android:name=\".KitsuneApplication\"\n        android:allowBackup=\"true\"\n        android:fullBackupContent=\"@xml/backup_rules\"\n        android:dataExtractionRules=\"@xml/backup_rules_sdk31\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.Kitsune.DayNight\"\n        android:hasFragileUserData=\"true\"\n        android:enableOnBackInvokedCallback=\"true\"\n        tools:targetApi=\"tiramisu\">\n        <profileable\n            android:shell=\"true\"\n            tools:targetApi=\"q\" />\n\n        <activity\n            android:name=\".ui.main.MainActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/Theme.Kitsune.SplashScreen.DayNight\"\n            android:windowSoftInputMode=\"adjustPan\">\n            <nav-graph android:value=\"@navigation/main_nav_graph\" />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.app.shortcuts\"\n                android:resource=\"@xml/shortcuts\" />\n        </activity>\n        <activity\n            android:name=\".ui.onboarding.OnboardingActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.photoview.PhotoViewActivity\"\n            android:configChanges=\"orientation|keyboardHidden|screenSize\"\n            android:exported=\"false\"\n            android:theme=\"@style/Theme.Kitsune.DayNight.FullScreen\" />\n        <activity\n            android:name=\".ui.authentication.AuthenticationActivity\"\n            android:exported=\"false\"\n            android:windowSoftInputMode=\"adjustResize\"\n            android:label=\"@string/title_authentication\" />\n\n        <receiver android:name=\".ui.widget.KitsuneWidgetReceiver\" android:exported=\"false\">\n            <intent-filter>\n                <action android:name=\"android.appwidget.action.APPWIDGET_UPDATE\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.appwidget.provider\"\n                android:resource=\"@xml/library_widget_info\" />\n        </receiver>\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/filepaths\" />\n        </provider>\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    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/KitsuneApplication.kt",
    "content": "package io.github.drumber.kitsune\n\nimport android.app.Application\nimport androidx.appcompat.app.AppCompatDelegate\nimport com.algolia.instantsearch.core.InstantSearchTelemetry\nimport com.chibatching.kotpref.Kotpref\nimport com.chibatching.kotpref.livedata.asLiveData\nimport io.github.drumber.kitsune.data.presentation.model.appupdate.UpdateCheckResult\nimport io.github.drumber.kitsune.data.repository.AppUpdateRepository\nimport io.github.drumber.kitsune.di.appModule\nimport io.github.drumber.kitsune.domain.auth.IsUserLoggedInUseCase\nimport io.github.drumber.kitsune.domain.user.UpdateLocalUserUseCase\nimport io.github.drumber.kitsune.notification.NotificationChannels\nimport io.github.drumber.kitsune.notification.Notifications\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.util.logD\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.logI\nimport io.github.drumber.kitsune.util.logW\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.launch\nimport org.koin.android.ext.android.get\nimport org.koin.android.ext.android.inject\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.android.ext.koin.androidLogger\nimport org.koin.core.context.startKoin\nimport org.koin.core.logger.Level\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass KitsuneApplication : Application() {\n\n    private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)\n\n    override fun onCreate() {\n        super.onCreate()\n\n        try {\n            NotificationChannels.registerNotificationChannels(this)\n        } catch (e: Exception) {\n            logE(\"Failed to register notification channels.\", e)\n        }\n\n        startKoin {\n            androidLogger(if (BuildConfig.DEBUG) Level.DEBUG else Level.INFO)\n            androidContext(this@KitsuneApplication)\n            modules(appModule)\n        }\n\n        performMigrations()\n\n        Kotpref.init(this)\n\n        KitsunePref.asLiveData(KitsunePref::darkMode).observeForever {\n            AppCompatDelegate.setDefaultNightMode(it.toInt())\n        }\n\n        // opt out of algolia telemetry\n        InstantSearchTelemetry.shared.enabled = false\n\n        initLoggedInUser()\n\n        if (!BuildConfig.SCREENSHOT_MODE_ENABLED && KitsunePref.checkForUpdatesOnStart) {\n            checkForNewVersion()\n        }\n    }\n\n    private fun performMigrations() {\n        // 1.8.0 - replaced ResourceDatabase with LocalDatabase\n        listOf(\"resources.db\", \"resources.db-shm\", \"resources.db-wal\").forEach {\n            val databaseFile = getDatabasePath(it)\n            if (databaseFile.isFile) {\n                try {\n                    val isDeleted = databaseFile.delete()\n                    if (isDeleted)\n                        logI(\"[Migration-1.8.0] Deleted database file '${databaseFile.absolutePath}'.\")\n                    else\n                        logW(\"[Migration-1.8.0] Failed to delete database file '${databaseFile.absolutePath}'.\")\n                } catch (e: Exception) {\n                    logE(\n                        \"[Migration-1.8.0] Error while deleting database file '${databaseFile.absolutePath}'.\",\n                        e\n                    )\n                }\n            }\n        }\n    }\n\n    private fun initLoggedInUser() {\n        val isUserLoggedIn: IsUserLoggedInUseCase by inject()\n        if (isUserLoggedIn()) {\n            val updateLocalUser: UpdateLocalUserUseCase by inject()\n            applicationScope.launch(Dispatchers.IO) {\n                updateLocalUser()\n            }\n        }\n    }\n\n    private fun checkForNewVersion() {\n        val lastUpdateCheck = KitsunePref.lastUpdateCheck\n        val now = System.currentTimeMillis().milliseconds\n        if (lastUpdateCheck != -1L && now.minus(lastUpdateCheck.milliseconds) < 24.hours) {\n            logD(\"Skipping update check, last check was: $lastUpdateCheck\")\n            return\n        }\n\n        applicationScope.launch {\n            val updateChecker: AppUpdateRepository = get()\n            val result = updateChecker.checkForUpdates(BuildConfig.VERSION_NAME)\n            if (result is UpdateCheckResult.NewVersion) {\n                Notifications.showNewVersion(this@KitsuneApplication, result.release)\n            }\n            KitsunePref.lastUpdateCheck = System.currentTimeMillis()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/KitsuneGlideModule.kt",
    "content": "package io.github.drumber.kitsune\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.GlideBuilder\nimport com.bumptech.glide.Registry\nimport com.bumptech.glide.RequestBuilder\nimport com.bumptech.glide.annotation.GlideModule\nimport com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader\nimport com.bumptech.glide.load.MultiTransformation\nimport com.bumptech.glide.load.Transformation\nimport com.bumptech.glide.load.model.GlideUrl\nimport com.bumptech.glide.load.resource.bitmap.CenterCrop\nimport com.bumptech.glide.module.AppGlideModule\nimport com.bumptech.glide.request.RequestOptions\nimport okhttp3.OkHttpClient\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.get\nimport org.koin.core.qualifier.named\nimport java.io.InputStream\n\n@GlideModule\nclass KitsuneGlideModule : AppGlideModule(), KoinComponent {\n\n    override fun applyOptions(context: Context, builder: GlideBuilder) {\n        val multiTransform = MultiTransformation(\n            buildList {\n                add(CenterCrop())\n                if (BuildConfig.SCREENSHOT_MODE_ENABLED) {\n                    val blurTransformation =\n                        Class.forName(\"jp.wasabeef.glide.transformations.BlurTransformation\")\n                            .getConstructor(Integer.TYPE, Integer.TYPE)\n                            .newInstance(15, 2)\n                    add(blurTransformation as Transformation<Bitmap>)\n                }\n            }\n        )\n        builder.setDefaultRequestOptions(RequestOptions.bitmapTransform(multiTransform))\n    }\n\n    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {\n        // replace Glides default OkHttpClient\n        val okHttpClient: OkHttpClient = get(named(\"images\"))\n        registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(okHttpClient))\n    }\n\n}\n\nfun <T> RequestBuilder<T>.addTransform(vararg transformations: Transformation<Bitmap>) = with(this) {\n    val oldTransforms = this.transformations\n        .filterKeys { it.isAssignableFrom(Bitmap::class.java) }\n        .map { it.value as Transformation<Bitmap> }\n    transform(MultiTransformation(oldTransforms + transformations))\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/AppTheme.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nimport androidx.annotation.StyleRes\nimport io.github.drumber.kitsune.R\n\nenum class AppTheme(@StyleRes val themeRes: Int, @StyleRes val blackThemeRes: Int) {\n    DEFAULT(R.style.Theme_Kitsune_DayNight, R.style.Theme_Kitsune_DayNight_Black),\n    PURPLE(R.style.Theme_Kitsune_DayNight_Purple, R.style.Theme_Kitsune_DayNight_Purple_Black),\n    BLUE(R.style.Theme_Kitsune_DayNight_Blue, R.style.Theme_Kitsune_DayNight_Blue_Black),\n    GREEN(R.style.Theme_Kitsune_DayNight_Green, R.style.Theme_Kitsune_DayNight_Green_Black),\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/Defaults.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nobject Defaults {\n\n    /** The minimum of required fields to display resources in a collection, e.g. in RecyclerView. */\n    val MINIMUM_COLLECTION_FIELDS get() = arrayOf(\"slug\", \"titles\", \"canonicalTitle\", \"posterImage\", \"coverImage\")\n\n    val MINIMUM_CHARACTER_FIELDS get() = arrayOf(\"slug\", \"name\", \"image\")\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/GitHub.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nobject GitHub {\n\n    const val API_URL = \"https://api.github.com/repos/drumber/kitsune/\"\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/IntentAction.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nobject IntentAction {\n    // Actions for app shortcuts\n    const val SHORTCUT_LIBRARY = \"io.github.drumber.kitsune.LIBRARY\"\n    const val SHORTCUT_SEARCH = \"io.github.drumber.kitsune.SEARCH\"\n    const val SHORTCUT_SETTINGS = \"io.github.drumber.kitsune.SETTINGS\"\n\n    // Actions for widget\n    const val OPEN_MEDIA = \"io.github.drumber.kitsune.OPEN_MEDIA\"\n    const val OPEN_LIBRARY = \"io.github.drumber.kitsune.OPEN_LIBRARY\"\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/Kitsu.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nobject Kitsu {\n\n    const val DEFAULT_PAGE_OFFSET = 0\n    const val DEFAULT_PAGE_SIZE = 10\n    const val DEFAULT_PAGE_SIZE_LIBRARY = 30\n\n    const val ALGOLIA_APP_ID = \"AWQO5J657S\"\n\n    const val API_HOST = \"kitsu.app\"\n    const val API_URL = \"https://$API_HOST/api/edge/\"\n    const val OAUTH_URL = \"https://$API_HOST/api/oauth/\"\n    const val BASE_URL = \"https://$API_HOST\"\n    const val USER_URL_PREFIX = \"$BASE_URL/users/\"\n    const val ANIME_URL_PREFIX = \"$BASE_URL/anime/\"\n\n    const val MANGA_URL_PREFIX = \"$BASE_URL/manga/\"\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/LibraryWidget.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nobject LibraryWidget {\n    const val MAX_ITEM_COUNT = 10\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/MediaItemSize.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nimport androidx.annotation.DimenRes\nimport io.github.drumber.kitsune.R\n\nenum class MediaItemSize(\n    @DimenRes val widthRes: Int,\n    @DimenRes val heightRes: Int\n) {\n    SMALL(R.dimen.media_item_width_small, R.dimen.media_item_height_small),\n    MEDIUM(R.dimen.media_item_width_medium, R.dimen.media_item_height_medium),\n    LARGE(R.dimen.media_item_width_large, R.dimen.media_item_height_large)\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/Repository.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nobject Repository {\n\n    const val MAX_CACHED_ITEMS = 600\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/SortFilter.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nimport io.github.drumber.kitsune.R\n\nenum class SortFilter(val queryParam: String) {\n\n    POPULARITY_DESC(\"-user_count\"),\n    POPULARITY_ASC(\"user_count\"),\n\n    AVERAGE_RATING_DESC(\"-average_rating\"),\n    AVERAGE_RATING_ASC(\"average_rating\"),\n\n    DATE_DESC(\"-start_date\"),\n    DATE_ASC(\"start_date\");\n\n    companion object {\n        fun fromQueryParam(queryParam: String?): SortFilter? {\n            if(queryParam != null) {\n                entries.forEach {\n                    if(it.queryParam.startsWith(queryParam)) {\n                        return it\n                    }\n                }\n            }\n            return null\n        }\n    }\n\n}\n\nenum class CategorySortFilter(val queryParam: String) {\n    TOTAL_MEDIA_COUNT_DESC(\"-total_media_count\"),\n    TOTAL_MEDIA_COUNT_ASC(\"total_media_count\")\n}\n\nfun SortFilter.toStringRes() = when (this) {\n    SortFilter.POPULARITY_DESC -> R.string.sort_popularity_desc\n    SortFilter.POPULARITY_ASC -> R.string.sort_popularity_asc\n    SortFilter.AVERAGE_RATING_DESC -> R.string.sort_average_rating_desc\n    SortFilter.AVERAGE_RATING_ASC -> R.string.sort_average_rating_asc\n    SortFilter.DATE_DESC -> R.string.sort_release_date_desc\n    SortFilter.DATE_ASC -> R.string.sort_release_date_asc\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/constants/StreamingLogo.kt",
    "content": "package io.github.drumber.kitsune.constants\n\nimport androidx.annotation.DrawableRes\nimport io.github.drumber.kitsune.R\n\nenum class StreamingLogo(@DrawableRes val drawable: Int) {\n\n    Amazon(R.drawable.ic_amazon),\n    Animelab(R.drawable.ic_animelab),\n    CONtv(R.drawable.ic_contv),\n    Crunchyroll(R.drawable.ic_crunchyroll),\n    Funimation(R.drawable.ic_funimation),\n    HIDIVE(R.drawable.ic_hidive),\n    Hulu(R.drawable.ic_hulu),\n    Netflix(R.drawable.ic_netflix),\n    TubiTV(R.drawable.ic_tubitv),\n    VRV(R.drawable.ic_vrv),\n    YouTube(R.drawable.ic_youtube)\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/Filter.kt",
    "content": "package io.github.drumber.kitsune.data.common\n\nimport io.github.drumber.kitsune.constants.Kitsu\n\ndata class Filter(val options: FilterOptions = mutableMapOf()) {\n\n    /** Defines how much of data to receive. This is only used for some lists, like trending. */\n    fun limit(limit: Int) = put(\"limit\", limit)\n\n    /** Defines how much of a resource to receive. */\n    fun pageLimit(limit: Int = Kitsu.DEFAULT_PAGE_SIZE) = put(\"page[limit]\", limit)\n\n    /** The index of the first resource to receive. */\n    fun pageOffset(offset: Int = Kitsu.DEFAULT_PAGE_OFFSET) = put(\"page[offset]\", offset)\n\n    /** Return only the specified fields of a resource. */\n    fun fields(type: String, vararg fields: String) = put(\"fields[$type]\", fields.joinToString(\",\"))\n\n    /** Sort by the specified attributes. Prepend a `-` for descending order. */\n    fun sort(vararg attributes: String) = put(\"sort\", attributes.joinToString(\",\"))\n\n    /** Filter by certain matching attributes or relationships. */\n    fun filter(attribute: String, value: String) = put(\"filter[$attribute]\", value)\n\n    /** Checks if there is a filter applied for the given attribute name. */\n    fun hasFilterAttribute(attribute: String) = options.containsKey(\"filter[$attribute]\")\n\n    fun include(vararg relationships: String) = put(\"include\", relationships.joinToString(\",\"))\n\n    private fun put(key: String, value: Any): Filter {\n        options[key] = value.toString()\n        return this\n    }\n}\n\ntypealias FilterOptions = MutableMap<String, String>\n\nfun FilterOptions.toFilter() = Filter(this)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/Image.kt",
    "content": "package io.github.drumber.kitsune.data.common\n\ndata class Image(\n    val tiny: String?,\n    val small: String?,\n    val medium: String?,\n    val large: String?,\n    val original: String?,\n    val meta: ImageMeta?\n) {\n    fun smallOrHigher(): String? {\n        return small ?: medium ?: large ?: original\n    }\n\n    fun largeOrDown(): String? {\n        return large ?: medium ?: small ?: tiny\n    }\n\n    fun originalOrDown(): String? {\n        return original ?: largeOrDown()\n    }\n}\n\ndata class ImageMeta(val dimensions: ImageDimensions?)\n\ndata class ImageDimensions(\n    val tiny: ImageDimension?,\n    val small: ImageDimension?,\n    val medium: ImageDimension?,\n    val large: ImageDimension?\n)\n\ndata class ImageDimension(val width: Int?, val height: Int?)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/Titles.kt",
    "content": "package io.github.drumber.kitsune.data.common\n\ntypealias Titles = Map<String, String?>\n\nval Titles.en get() = get(\"en\")\nval Titles.enUs get() = get(\"en_us\")\nval Titles.enJp get() = get(\"en_jp\")\nval Titles.jaJp get() = get(\"ja_jp\")\n\nprivate val commonTitleKeys = listOf(\"en\", \"en_jp\", \"ja_jp\")\nfun Titles.withoutCommonTitles() =\n    filterKeys { !commonTitleKeys.contains(it) }\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/exception/InvalidDataException.kt",
    "content": "package io.github.drumber.kitsune.data.common.exception\n\nclass InvalidDataException(message: String): Exception(message)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/exception/NoDataException.kt",
    "content": "package io.github.drumber.kitsune.data.common.exception\n\nclass NoDataException : Exception {\n    constructor() : super()\n    constructor(message: String) : super(message)\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/exception/NotFoundException.kt",
    "content": "package io.github.drumber.kitsune.data.common.exception\n\nclass NotFoundException(message: String? = null, cause: Throwable? = null) :\n    Exception(message, cause)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/exception/ResourceUpdateFailed.kt",
    "content": "package io.github.drumber.kitsune.data.common.exception\n\nclass ResourceUpdateFailed : Exception {\n    constructor() : super()\n    constructor(message: String) : super(message)\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/exception/SearchProviderUnavailableException.kt",
    "content": "package io.github.drumber.kitsune.data.common.exception\n\nclass SearchProviderUnavailableException(message: String? = null): Exception(message)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/library/LibraryEntryKind.kt",
    "content": "package io.github.drumber.kitsune.data.common.library\n\nenum class LibraryEntryKind {\n    All,\n    Anime,\n    Manga\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/media/AgeRating.kt",
    "content": "package io.github.drumber.kitsune.data.common.media\n\nenum class AgeRating {\n    G,\n    PG,\n    R,\n    R18\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/media/AnimeSubtype.kt",
    "content": "package io.github.drumber.kitsune.data.common.media\n\nenum class AnimeSubtype {\n    ONA,\n    OVA,\n    TV,\n    Movie,\n    Music,\n    Special\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/media/MangaSubtype.kt",
    "content": "package io.github.drumber.kitsune.data.common.media\n\nenum class MangaSubtype {\n    Doujin,\n    Manga,\n    Manhua,\n    Manhwa,\n    Novel,\n    Oel,\n    Oneshot\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/media/MediaType.kt",
    "content": "package io.github.drumber.kitsune.data.common.media\n\nenum class MediaType {\n    Anime,\n    Manga\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/media/RatingFrequencies.kt",
    "content": "package io.github.drumber.kitsune.data.common.media\n\ndata class RatingFrequencies(\n    val r2: String?,\n    val r3: String?,\n    val r4: String?,\n    val r5: String?,\n    val r6: String?,\n    val r7: String?,\n    val r8: String?,\n    val r9: String?,\n    val r10: String?,\n    val r11: String?,\n    val r12: String?,\n    val r13: String?,\n    val r14: String?,\n    val r15: String?,\n    val r16: String?,\n    val r17: String?,\n    val r18: String?,\n    val r19: String?,\n    val r20: String?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/media/ReleaseStatus.kt",
    "content": "package io.github.drumber.kitsune.data.common.media\n\nenum class ReleaseStatus {\n    Current,\n    Finished,\n    TBA,\n    Unreleased,\n    Upcoming\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/common/user/UserThemePreference.kt",
    "content": "package io.github.drumber.kitsune.data.common.user\n\nenum class UserThemePreference {\n    Dark, Light\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/AlgoliaMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.ImageDimension\nimport io.github.drumber.kitsune.data.common.ImageDimensions\nimport io.github.drumber.kitsune.data.common.ImageMeta\nimport io.github.drumber.kitsune.data.common.media.AnimeSubtype\nimport io.github.drumber.kitsune.data.common.media.MangaSubtype\nimport io.github.drumber.kitsune.data.presentation.model.algolia.AlgoliaKey\nimport io.github.drumber.kitsune.data.presentation.model.algolia.AlgoliaKeyCollection\nimport io.github.drumber.kitsune.data.presentation.model.character.CharacterSearchResult\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.source.network.algolia.model.NetworkAlgoliaKey\nimport io.github.drumber.kitsune.data.source.network.algolia.model.NetworkAlgoliaKeyCollection\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaCharacterSearchResult\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaDimension\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaDimensions\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaImage\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaImageMeta\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaMediaSearchKind\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaMediaSearchResult\n\nobject AlgoliaMapper {\n    fun NetworkAlgoliaKeyCollection.toAlgoliaKeyCollection() = AlgoliaKeyCollection(\n        users = users?.toAlgoliaKey(),\n        posts = posts?.toAlgoliaKey(),\n        media = media?.toAlgoliaKey(),\n        groups = groups?.toAlgoliaKey(),\n        characters = characters?.toAlgoliaKey()\n    )\n\n    fun NetworkAlgoliaKey.toAlgoliaKey() = AlgoliaKey(\n        key = key.require(),\n        index = index\n    )\n\n    fun AlgoliaMediaSearchResult.toMedia() = when (kind) {\n        AlgoliaMediaSearchKind.Anime -> toAnime()\n        AlgoliaMediaSearchKind.Manga -> toManga()\n    }\n\n    private fun AlgoliaMediaSearchResult.toAnime() = Anime(\n        id = id.toString(),\n        subtype = animeSubtypeFromString(subtype),\n        slug = slug,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        posterImage = posterImage?.toImage(),\n        abbreviatedTitles = null,\n        ageRating = null,\n        ageRatingGuide = null,\n        averageRating = null,\n        animeProduction = null,\n        categories = null,\n        coverImage = null,\n        description = null,\n        endDate = null,\n        episodeCount = null,\n        episodeLength = null,\n        favoritesCount = null,\n        mediaRelationships = null,\n        nextRelease = null,\n        nsfw = null,\n        popularityRank = null,\n        ratingFrequencies = null,\n        ratingRank = null,\n        startDate = null,\n        status = null,\n        streamingLinks = null,\n        tba = null,\n        totalLength = null,\n        userCount = null,\n        youtubeVideoId = null\n    )\n\n    private fun AlgoliaMediaSearchResult.toManga() = Manga(\n        id = id.toString(),\n        subtype = mangaSubtypeFromString(subtype),\n        slug = slug,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        posterImage = posterImage?.toImage(),\n        userCount = null,\n        totalLength = null,\n        tba = null,\n        status = null,\n        startDate = null,\n        ratingRank = null,\n        ratingFrequencies = null,\n        popularityRank = null,\n        nsfw = null,\n        nextRelease = null,\n        mediaRelationships = null,\n        favoritesCount = null,\n        endDate = null,\n        description = null,\n        coverImage = null,\n        categories = null,\n        averageRating = null,\n        ageRatingGuide = null,\n        ageRating = null,\n        abbreviatedTitles = null,\n        chapterCount = null,\n        serialization = null,\n        volumeCount = null\n    )\n\n    fun AlgoliaCharacterSearchResult.toCharacterSearchResult() = CharacterSearchResult(\n        id = id.toString(),\n        slug = slug,\n        name = canonicalName,\n        image = image?.toImage(),\n        primaryMediaTitle = primaryMedia\n    )\n\n    fun AlgoliaImage.toImage() = Image(\n        tiny, small, medium, large, original, meta?.toImageMeta()\n    )\n\n    fun AlgoliaImageMeta.toImageMeta() = ImageMeta(dimensions?.toDimensions())\n    fun AlgoliaDimensions.toDimensions() = ImageDimensions(\n        tiny?.toDimension(), small?.toDimension(), medium?.toDimension(), large?.toDimension()\n    )\n\n    fun AlgoliaDimension.toDimension() = ImageDimension(width, height)\n\n    private fun animeSubtypeFromString(subtype: String?) = AnimeSubtype.entries\n        .find { it.name.equals(subtype, true) }\n\n    private fun mangaSubtypeFromString(subtype: String?) = MangaSubtype.entries\n        .find { it.name.equals(subtype, true) }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/AppUpdateMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.presentation.model.appupdate.AppRelease\nimport io.github.drumber.kitsune.data.source.network.appupdate.model.NetworkGitHubRelease\n\nobject AppUpdateMapper {\n    fun NetworkGitHubRelease.toAppRelease() = AppRelease(\n        version = version,\n        url = url\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/AuthMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.NetworkAccessToken\n\nobject AuthMapper {\n    fun NetworkAccessToken.toLocalAccessToken() = LocalAccessToken(\n        accessToken = accessToken.require(),\n        createdAt = createdAt.require(),\n        expiresIn = expiresIn.require(),\n        refreshToken = refreshToken.require()\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/CharacterMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toMedia\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.presentation.model.character.CharacterSearchResult\nimport io.github.drumber.kitsune.data.presentation.model.character.MediaCharacter\nimport io.github.drumber.kitsune.data.presentation.model.character.MediaCharacterRole\nimport io.github.drumber.kitsune.data.source.local.character.LocalCharacter\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkMediaCharacter\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkMediaCharacterRole\n\nobject CharacterMapper {\n\n    //********************************************************************************************//\n    // From Network\n    //********************************************************************************************//\n\n    fun NetworkCharacter.toCharacter() = Character(\n        id = id.require(),\n        slug = slug,\n        name = name,\n        names = names,\n        otherNames = otherNames,\n        malId = malId,\n        description = description,\n        image = image,\n        mediaCharacters = mediaCharacters?.map { it.toMediaCharacter() }\n    )\n\n    fun NetworkCharacter.toLocalCharacter() = LocalCharacter(\n        id = id.require(),\n        slug = slug,\n        name = name,\n        names = names,\n        otherNames = otherNames,\n        malId = malId,\n        description = description,\n        image = image\n    )\n\n    fun CharacterSearchResult.toCharacter() = Character(\n        id = id,\n        slug = slug,\n        name = name,\n        names = null,\n        otherNames = null,\n        malId = null,\n        description = null,\n        image = image,\n        mediaCharacters = null\n    )\n\n    private fun NetworkMediaCharacter.toMediaCharacter() = MediaCharacter(\n        id = id.require(),\n        role = role?.toMediaCharacterRole(),\n        media = media?.toMedia()\n    )\n\n    private fun NetworkMediaCharacterRole.toMediaCharacterRole() = when (this) {\n        NetworkMediaCharacterRole.MAIN -> MediaCharacterRole.MAIN\n        NetworkMediaCharacterRole.SUPPORTING -> MediaCharacterRole.SUPPORTING\n        NetworkMediaCharacterRole.RECURRING -> MediaCharacterRole.RECURRING\n        NetworkMediaCharacterRole.CAMEO -> MediaCharacterRole.CAMEO\n    }\n\n    //********************************************************************************************//\n    // From Local\n    //********************************************************************************************//\n\n    fun LocalCharacter.toCharacter() = Character(\n        id = id.require(),\n        slug = slug,\n        name = name,\n        names = names,\n        otherNames = otherNames,\n        malId = malId,\n        description = description,\n        image = image,\n        mediaCharacters = null\n    )\n\n    fun LocalCharacter.toNetworkCharacter() = NetworkCharacter(\n        id = id,\n        slug = slug,\n        name = name,\n        names = names,\n        otherNames = otherNames,\n        malId = malId,\n        description = description,\n        image = image,\n        mediaCharacters = null\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/ImageMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.ImageDimension\nimport io.github.drumber.kitsune.data.common.ImageDimensions\nimport io.github.drumber.kitsune.data.common.ImageMeta\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalDimension\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalDimensions\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalImage\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalImageMeta\n\nobject ImageMapper {\n    fun Image.toLocalImage() = LocalImage(\n        tiny = tiny,\n        small = small,\n        medium = medium,\n        large = large,\n        original = original,\n        meta = meta?.toLocalImageMeta()\n    )\n\n    fun ImageMeta.toLocalImageMeta() = LocalImageMeta(\n        dimensions = dimensions?.toLocalDimensions()\n    )\n\n    fun ImageDimensions.toLocalDimensions() = LocalDimensions(\n        tiny = tiny?.toLocalDimension(),\n        small = small?.toLocalDimension(),\n        medium = medium?.toLocalDimension(),\n        large = large?.toLocalDimension()\n    )\n\n    fun ImageDimension.toLocalDimension() = LocalDimension(\n        width = width,\n        height = height\n    )\n\n    fun LocalImage.toImage() = Image(\n        tiny = tiny,\n        small = small,\n        medium = medium,\n        large = large,\n        original = original,\n        meta = meta?.toImageMeta()\n    )\n\n    fun LocalImageMeta.toImageMeta() = ImageMeta(\n        dimensions = dimensions?.toImageDimensions()\n    )\n\n    fun LocalDimensions.toImageDimensions() = ImageDimensions(\n        tiny = tiny?.toImageDimension(),\n        small = small?.toImageDimension(),\n        medium = medium?.toImageDimension(),\n        large = large?.toImageDimension()\n    )\n\n    fun LocalDimension.toImageDimension() = ImageDimension(\n        width = width,\n        height = height\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/LibraryMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.mapper.ImageMapper.toImage\nimport io.github.drumber.kitsune.data.mapper.ImageMapper.toLocalImage\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toAnime\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toAnimeSubtype\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toManga\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toMangaSubtype\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toRatingFrequencies\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toReleaseStatus\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryModificationState\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.library.ReactionSkip\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntry\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntryModification\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryMedia\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryMedia.MediaType\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryModificationState\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryStatus\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalReactionSkip\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryEntry\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryStatus\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkReactionSkip\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\n\nobject LibraryMapper {\n    fun LocalLibraryEntry.toLibraryEntry() = LibraryEntry(\n        id = id,\n        updatedAt = updatedAt,\n        startedAt = startedAt,\n        finishedAt = finishedAt,\n        progressedAt = progressedAt,\n        status = status?.toLibraryStatus(),\n        progress = progress,\n        reconsuming = reconsuming,\n        reconsumeCount = reconsumeCount,\n        volumesOwned = volumesOwned,\n        ratingTwenty = ratingTwenty,\n        notes = notes,\n        privateEntry = privateEntry,\n        reactionSkipped = reactionSkipped?.toReactionSkip(),\n        media = media?.toMedia()\n    )\n\n    fun NetworkLibraryEntry.toLibraryEntry() = LibraryEntry(\n        id = id.require(),\n        updatedAt = updatedAt,\n        startedAt = startedAt,\n        finishedAt = finishedAt,\n        progressedAt = progressedAt,\n        status = status?.toLibraryStatus(),\n        progress = progress,\n        reconsuming = reconsuming,\n        reconsumeCount = reconsumeCount,\n        volumesOwned = volumesOwned,\n        ratingTwenty = ratingTwenty,\n        notes = notes,\n        privateEntry = privateEntry,\n        reactionSkipped = reactionSkipped?.toReactionSkip(),\n        media = when {\n            anime != null -> anime.toAnime()\n            manga != null -> manga.toManga()\n            else -> null\n        }\n    )\n\n    fun NetworkLibraryEntry.toLocalLibraryEntry() = LocalLibraryEntry(\n        id = id.require(),\n        updatedAt = updatedAt,\n        startedAt = startedAt,\n        finishedAt = finishedAt,\n        progressedAt = progressedAt,\n        status = status?.toLocalLibraryStatus(),\n        progress = progress,\n        reconsuming = reconsuming,\n        reconsumeCount = reconsumeCount,\n        volumesOwned = volumesOwned,\n        ratingTwenty = ratingTwenty,\n        notes = notes,\n        privateEntry = privateEntry,\n        reactionSkipped = reactionSkipped?.toLocalReactionSkip(),\n        media = when {\n            anime != null -> anime.toLocalLibraryMedia()\n            manga != null -> manga.toLocalLibraryMedia()\n            else -> null\n        }\n    )\n\n    fun LocalLibraryEntryModification.toLibraryEntryModification() = LibraryEntryModification(\n        id = id,\n        createTime = createTime,\n        state = state.toLibraryModificationState(),\n        startedAt = startedAt,\n        finishedAt = finishedAt,\n        status = status?.toLibraryStatus(),\n        progress = progress,\n        reconsumeCount = reconsumeCount,\n        volumesOwned = volumesOwned,\n        ratingTwenty = ratingTwenty,\n        notes = notes,\n        privateEntry = privateEntry,\n    )\n\n    fun LibraryEntryModification.toLocalLibraryEntryModification() = LocalLibraryEntryModification(\n        id = id,\n        createTime = createTime,\n        state = state.toLocalLibraryModificationState(),\n        startedAt = startedAt,\n        finishedAt = finishedAt,\n        status = status?.toLocalLibraryStatus(),\n        progress = progress,\n        reconsumeCount = reconsumeCount,\n        volumesOwned = volumesOwned,\n        ratingTwenty = ratingTwenty,\n        notes = notes,\n        privateEntry = privateEntry,\n    )\n\n    fun LocalLibraryMedia.toMedia() = when (type) {\n        MediaType.Anime -> toAnime()\n        MediaType.Manga -> toManga()\n    }\n\n    fun LocalLibraryMedia.toAnime() = Anime(\n        id = id,\n        slug = null,\n        description = description,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        abbreviatedTitles = abbreviatedTitles,\n        averageRating = averageRating,\n        ratingFrequencies = ratingFrequencies,\n        userCount = null,\n        favoritesCount = null,\n        popularityRank = popularityRank,\n        ratingRank = ratingRank,\n        startDate = startDate,\n        endDate = endDate,\n        nextRelease = nextRelease,\n        tba = tba,\n        status = status,\n        ageRating = ageRating,\n        ageRatingGuide = ageRatingGuide,\n        nsfw = nsfw,\n        posterImage = posterImage?.toImage(),\n        coverImage = coverImage?.toImage(),\n        totalLength = totalLength,\n        episodeCount = episodeCount,\n        episodeLength = episodeLength,\n        youtubeVideoId = null,\n        subtype = animeSubtype,\n        categories = null,\n        animeProduction = null,\n        streamingLinks = null,\n        mediaRelationships = null\n    )\n\n    fun LocalLibraryMedia.toManga() = Manga(\n        id = id,\n        slug = null,\n        description = description,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        abbreviatedTitles = abbreviatedTitles,\n        averageRating = averageRating,\n        ratingFrequencies = ratingFrequencies,\n        userCount = null,\n        favoritesCount = null,\n        popularityRank = popularityRank,\n        ratingRank = ratingRank,\n        startDate = startDate,\n        endDate = endDate,\n        nextRelease = nextRelease,\n        tba = tba,\n        status = status,\n        ageRating = ageRating,\n        ageRatingGuide = ageRatingGuide,\n        nsfw = nsfw,\n        posterImage = posterImage?.toImage(),\n        coverImage = coverImage?.toImage(),\n        totalLength = totalLength,\n        chapterCount = chapterCount,\n        volumeCount = volumeCount,\n        subtype = mangaSubtype,\n        serialization = serialization,\n        categories = null,\n        mediaRelationships = null\n    )\n\n    fun NetworkAnime.toLocalLibraryMedia() = LocalLibraryMedia(\n        id = id,\n        type = MediaType.Anime,\n        description = description,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        abbreviatedTitles = abbreviatedTitles,\n        averageRating = averageRating,\n        ratingFrequencies = ratingFrequencies?.toRatingFrequencies(),\n        popularityRank = popularityRank,\n        ratingRank = ratingRank,\n        startDate = startDate,\n        endDate = endDate,\n        nextRelease = nextRelease,\n        tba = tba,\n        status = status?.toReleaseStatus(),\n        ageRating = ageRating,\n        ageRatingGuide = ageRatingGuide,\n        nsfw = nsfw,\n        posterImage = posterImage?.toLocalImage(),\n        coverImage = coverImage?.toLocalImage(),\n        animeSubtype = subtype?.toAnimeSubtype(),\n        totalLength = totalLength,\n        episodeCount = episodeCount,\n        episodeLength = episodeLength,\n        mangaSubtype = null,\n        chapterCount = null,\n        volumeCount = null,\n        serialization = null\n    )\n\n    fun NetworkManga.toLocalLibraryMedia() = LocalLibraryMedia(\n        id = id,\n        type = MediaType.Manga,\n        description = description,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        abbreviatedTitles = abbreviatedTitles,\n        averageRating = averageRating,\n        ratingFrequencies = ratingFrequencies?.toRatingFrequencies(),\n        popularityRank = popularityRank,\n        ratingRank = ratingRank,\n        startDate = startDate,\n        endDate = endDate,\n        nextRelease = nextRelease,\n        tba = tba,\n        status = status?.toReleaseStatus(),\n        ageRating = ageRating,\n        ageRatingGuide = ageRatingGuide,\n        nsfw = nsfw,\n        posterImage = posterImage?.toLocalImage(),\n        coverImage = coverImage?.toLocalImage(),\n        animeSubtype = null,\n        totalLength = null,\n        episodeCount = null,\n        episodeLength = null,\n        mangaSubtype = subtype?.toMangaSubtype(),\n        chapterCount = chapterCount,\n        volumeCount = volumeCount,\n        serialization = serialization\n    )\n\n    fun NetworkReactionSkip.toLocalReactionSkip(): LocalReactionSkip = when (this) {\n        NetworkReactionSkip.Unskipped -> LocalReactionSkip.Unskipped\n        NetworkReactionSkip.Skipped -> LocalReactionSkip.Skipped\n        NetworkReactionSkip.Ignored -> LocalReactionSkip.Ignored\n    }\n\n    fun NetworkLibraryStatus.toLocalLibraryStatus(): LocalLibraryStatus = when (this) {\n        NetworkLibraryStatus.Current -> LocalLibraryStatus.Current\n        NetworkLibraryStatus.Planned -> LocalLibraryStatus.Planned\n        NetworkLibraryStatus.Completed -> LocalLibraryStatus.Completed\n        NetworkLibraryStatus.OnHold -> LocalLibraryStatus.OnHold\n        NetworkLibraryStatus.Dropped -> LocalLibraryStatus.Dropped\n    }\n\n    fun NetworkLibraryStatus.toLibraryStatus(): LibraryStatus = when (this) {\n        NetworkLibraryStatus.Current -> LibraryStatus.Current\n        NetworkLibraryStatus.Planned -> LibraryStatus.Planned\n        NetworkLibraryStatus.Completed -> LibraryStatus.Completed\n        NetworkLibraryStatus.OnHold -> LibraryStatus.OnHold\n        NetworkLibraryStatus.Dropped -> LibraryStatus.Dropped\n    }\n\n    fun LibraryStatus.toLocalLibraryStatus(): LocalLibraryStatus = when (this) {\n        LibraryStatus.Current -> LocalLibraryStatus.Current\n        LibraryStatus.Planned -> LocalLibraryStatus.Planned\n        LibraryStatus.Completed -> LocalLibraryStatus.Completed\n        LibraryStatus.OnHold -> LocalLibraryStatus.OnHold\n        LibraryStatus.Dropped -> LocalLibraryStatus.Dropped\n    }\n\n    fun LibraryStatus.toNetworkLibraryStatus(): NetworkLibraryStatus = when (this) {\n        LibraryStatus.Current -> NetworkLibraryStatus.Current\n        LibraryStatus.Planned -> NetworkLibraryStatus.Planned\n        LibraryStatus.Completed -> NetworkLibraryStatus.Completed\n        LibraryStatus.OnHold -> NetworkLibraryStatus.OnHold\n        LibraryStatus.Dropped -> NetworkLibraryStatus.Dropped\n    }\n\n    fun LocalLibraryStatus.toLibraryStatus(): LibraryStatus = when (this) {\n        LocalLibraryStatus.Current -> LibraryStatus.Current\n        LocalLibraryStatus.Planned -> LibraryStatus.Planned\n        LocalLibraryStatus.Completed -> LibraryStatus.Completed\n        LocalLibraryStatus.OnHold -> LibraryStatus.OnHold\n        LocalLibraryStatus.Dropped -> LibraryStatus.Dropped\n    }\n\n    fun NetworkReactionSkip.toReactionSkip(): ReactionSkip = when (this) {\n        NetworkReactionSkip.Unskipped -> ReactionSkip.Unskipped\n        NetworkReactionSkip.Skipped -> ReactionSkip.Skipped\n        NetworkReactionSkip.Ignored -> ReactionSkip.Ignored\n    }\n\n    fun LocalReactionSkip.toReactionSkip(): ReactionSkip = when (this) {\n        LocalReactionSkip.Unskipped -> ReactionSkip.Unskipped\n        LocalReactionSkip.Skipped -> ReactionSkip.Skipped\n        LocalReactionSkip.Ignored -> ReactionSkip.Ignored\n    }\n\n    fun LocalLibraryModificationState.toLibraryModificationState(): LibraryModificationState =\n        when (this) {\n            LocalLibraryModificationState.SYNCHRONIZING -> LibraryModificationState.SYNCHRONIZING\n            LocalLibraryModificationState.NOT_SYNCHRONIZED -> LibraryModificationState.NOT_SYNCHRONIZED\n        }\n\n    fun LibraryModificationState.toLocalLibraryModificationState(): LocalLibraryModificationState =\n        when (this) {\n            LibraryModificationState.SYNCHRONIZING -> LocalLibraryModificationState.SYNCHRONIZING\n            LibraryModificationState.NOT_SYNCHRONIZED -> LocalLibraryModificationState.NOT_SYNCHRONIZED\n        }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/MapperUtils.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\ninternal inline fun <reified T> T?.require(): T {\n    return this ?: throw MappingException(\"Required value is null\")\n}\n\nclass MappingException(message: String) : IllegalArgumentException(message)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/MappingMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.presentation.model.mapping.Mapping\nimport io.github.drumber.kitsune.data.source.network.mapping.model.NetworkMapping\n\nobject MappingMapper {\n    fun NetworkMapping.toMapping() = Mapping(\n        id = id.require(),\n        externalSite = externalSite,\n        externalId = externalId\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/MediaMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.common.media.AnimeSubtype\nimport io.github.drumber.kitsune.data.common.media.MangaSubtype\nimport io.github.drumber.kitsune.data.common.media.RatingFrequencies\nimport io.github.drumber.kitsune.data.common.media.ReleaseStatus\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toCharacter\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.presentation.model.media.category.Category\nimport io.github.drumber.kitsune.data.presentation.model.media.production.AnimeProduction\nimport io.github.drumber.kitsune.data.presentation.model.media.production.AnimeProductionRole\nimport io.github.drumber.kitsune.data.presentation.model.media.production.Casting\nimport io.github.drumber.kitsune.data.presentation.model.media.production.Person\nimport io.github.drumber.kitsune.data.presentation.model.media.production.Producer\nimport io.github.drumber.kitsune.data.presentation.model.media.relationship.MediaRelationship\nimport io.github.drumber.kitsune.data.presentation.model.media.relationship.MediaRelationshipRole\nimport io.github.drumber.kitsune.data.presentation.model.media.streamer.Streamer\nimport io.github.drumber.kitsune.data.presentation.model.media.streamer.StreamingLink\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnimeSubtype\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkMangaSubtype\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkMedia\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkRatingFrequencies\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkReleaseStatus\nimport io.github.drumber.kitsune.data.source.network.media.model.category.NetworkCategory\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkAnimeProduction\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkAnimeProductionRole\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkCasting\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkPerson\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkProducer\nimport io.github.drumber.kitsune.data.source.network.media.model.relationship.NetworkMediaRelationship\nimport io.github.drumber.kitsune.data.source.network.media.model.relationship.NetworkMediaRelationshipRole\nimport io.github.drumber.kitsune.data.source.network.media.model.streamer.NetworkStreamer\nimport io.github.drumber.kitsune.data.source.network.media.model.streamer.NetworkStreamingLink\n\nobject MediaMapper {\n    fun NetworkMedia.toMedia(): Media = when (this) {\n        is NetworkAnime -> toAnime()\n        is NetworkManga -> toManga()\n    }\n\n    fun NetworkAnime.toAnime() = Anime(\n        id = id,\n        slug = slug,\n        description = description,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        abbreviatedTitles = abbreviatedTitles,\n        averageRating = averageRating,\n        ratingFrequencies = ratingFrequencies?.toRatingFrequencies(),\n        userCount = userCount,\n        favoritesCount = favoritesCount,\n        popularityRank = popularityRank,\n        ratingRank = ratingRank,\n        startDate = startDate,\n        endDate = endDate,\n        nextRelease = nextRelease,\n        tba = tba,\n        status = status?.toReleaseStatus(),\n        ageRating = ageRating,\n        ageRatingGuide = ageRatingGuide,\n        nsfw = nsfw,\n        posterImage = posterImage,\n        coverImage = coverImage,\n        totalLength = totalLength,\n        episodeCount = episodeCount,\n        episodeLength = episodeLength,\n        youtubeVideoId = youtubeVideoId,\n        subtype = subtype?.toAnimeSubtype(),\n        categories = categories?.map { it.toCategory() },\n        animeProduction = animeProduction?.map { it.toAnimeProduction() },\n        streamingLinks = streamingLinks?.map { it.toStreamingLink() },\n        mediaRelationships = mediaRelationships?.map { it.toMediaRelationship() }\n    )\n\n    fun NetworkManga.toManga() = Manga(\n        id = id,\n        slug = slug,\n        description = description,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        abbreviatedTitles = abbreviatedTitles,\n        averageRating = averageRating,\n        ratingFrequencies = ratingFrequencies?.toRatingFrequencies(),\n        userCount = userCount,\n        favoritesCount = favoritesCount,\n        popularityRank = popularityRank,\n        ratingRank = ratingRank,\n        startDate = startDate,\n        endDate = endDate,\n        nextRelease = nextRelease,\n        tba = tba,\n        status = status?.toReleaseStatus(),\n        ageRating = ageRating,\n        ageRatingGuide = ageRatingGuide,\n        nsfw = nsfw,\n        posterImage = posterImage,\n        coverImage = coverImage,\n        totalLength = totalLength,\n        chapterCount = chapterCount,\n        volumeCount = volumeCount,\n        subtype = subtype?.toMangaSubtype(),\n        serialization = serialization,\n        categories = categories?.map { it.toCategory() },\n        mediaRelationships = mediaRelationships?.map { it.toMediaRelationship() }\n    )\n\n    fun NetworkRatingFrequencies.toRatingFrequencies() = RatingFrequencies(\n        r2 = r2,\n        r3 = r3,\n        r4 = r4,\n        r5 = r5,\n        r6 = r6,\n        r7 = r7,\n        r8 = r8,\n        r9 = r9,\n        r10 = r10,\n        r11 = r11,\n        r12 = r12,\n        r13 = r13,\n        r14 = r14,\n        r15 = r15,\n        r16 = r16,\n        r17 = r17,\n        r18 = r18,\n        r19 = r19,\n        r20 = r20\n    )\n\n    fun NetworkReleaseStatus.toReleaseStatus(): ReleaseStatus = when (this) {\n        NetworkReleaseStatus.Current -> ReleaseStatus.Current\n        NetworkReleaseStatus.Finished -> ReleaseStatus.Finished\n        NetworkReleaseStatus.TBA -> ReleaseStatus.TBA\n        NetworkReleaseStatus.Unreleased -> ReleaseStatus.Unreleased\n        NetworkReleaseStatus.Upcoming -> ReleaseStatus.Upcoming\n    }\n\n    fun NetworkAnimeSubtype.toAnimeSubtype(): AnimeSubtype = when (this) {\n        NetworkAnimeSubtype.ONA -> AnimeSubtype.ONA\n        NetworkAnimeSubtype.OVA -> AnimeSubtype.OVA\n        NetworkAnimeSubtype.TV -> AnimeSubtype.TV\n        NetworkAnimeSubtype.Movie -> AnimeSubtype.Movie\n        NetworkAnimeSubtype.Music -> AnimeSubtype.Music\n        NetworkAnimeSubtype.Special -> AnimeSubtype.Special\n    }\n\n    fun NetworkMangaSubtype.toMangaSubtype(): MangaSubtype = when (this) {\n        NetworkMangaSubtype.Doujin -> MangaSubtype.Doujin\n        NetworkMangaSubtype.Manga -> MangaSubtype.Manga\n        NetworkMangaSubtype.Manhua -> MangaSubtype.Manhua\n        NetworkMangaSubtype.Manhwa -> MangaSubtype.Manhwa\n        NetworkMangaSubtype.Novel -> MangaSubtype.Novel\n        NetworkMangaSubtype.Oel -> MangaSubtype.Oel\n        NetworkMangaSubtype.Oneshot -> MangaSubtype.Oneshot\n    }\n\n    //********************************************************************************************//\n    // Category\n    //********************************************************************************************//\n\n    fun NetworkCategory.toCategory() = Category(\n        id = id.require(),\n        slug = slug,\n        title = title,\n        description = description,\n        nsfw = nsfw,\n        totalMediaCount = totalMediaCount,\n        childCount = childCount\n    )\n\n    //********************************************************************************************//\n    // Production\n    //********************************************************************************************//\n\n    fun NetworkAnimeProduction.toAnimeProduction() = AnimeProduction(\n        id = id.require(),\n        role = role?.toAnimeProductionRole(),\n        producer = producer?.toProducer()\n    )\n\n    fun NetworkAnimeProductionRole.toAnimeProductionRole(): AnimeProductionRole = when (this) {\n        NetworkAnimeProductionRole.Licensor -> AnimeProductionRole.Licensor\n        NetworkAnimeProductionRole.Producer -> AnimeProductionRole.Producer\n        NetworkAnimeProductionRole.Studio -> AnimeProductionRole.Studio\n    }\n\n    fun NetworkProducer.toProducer() = Producer(\n        id = id.require(),\n        slug = slug,\n        name = name\n    )\n\n    fun NetworkCasting.toCasting() = Casting(\n        id = id.require(),\n        role = role,\n        voiceActor = voiceActor,\n        featured = featured,\n        language = language,\n        character = character?.toCharacter(),\n        person = person?.toPerson()\n    )\n\n    fun NetworkPerson.toPerson() = Person(\n        id = id.require(),\n        name = name,\n        description = description,\n        image = image\n    )\n\n    //********************************************************************************************//\n    // Relationship\n    //********************************************************************************************//\n\n    fun NetworkMediaRelationship.toMediaRelationship() = MediaRelationship(\n        id = id.require(),\n        role = role?.toMediaRelationshipRole(),\n        media = media?.toMedia()\n    )\n\n    fun NetworkMediaRelationshipRole.toMediaRelationshipRole(): MediaRelationshipRole =\n        when (this) {\n            NetworkMediaRelationshipRole.Sequel -> MediaRelationshipRole.Sequel\n            NetworkMediaRelationshipRole.Prequel -> MediaRelationshipRole.Prequel\n            NetworkMediaRelationshipRole.AlternativeSetting -> MediaRelationshipRole.AlternativeSetting\n            NetworkMediaRelationshipRole.AlternativeVersion -> MediaRelationshipRole.AlternativeVersion\n            NetworkMediaRelationshipRole.SideStory -> MediaRelationshipRole.SideStory\n            NetworkMediaRelationshipRole.ParentStory -> MediaRelationshipRole.ParentStory\n            NetworkMediaRelationshipRole.Summary -> MediaRelationshipRole.Summary\n            NetworkMediaRelationshipRole.FullStory -> MediaRelationshipRole.FullStory\n            NetworkMediaRelationshipRole.Spinoff -> MediaRelationshipRole.Spinoff\n            NetworkMediaRelationshipRole.Adaptation -> MediaRelationshipRole.Adaptation\n            NetworkMediaRelationshipRole.Character -> MediaRelationshipRole.Character\n            NetworkMediaRelationshipRole.Other -> MediaRelationshipRole.Other\n        }\n\n    //********************************************************************************************//\n    // Streamer\n    //********************************************************************************************//\n\n    fun NetworkStreamingLink.toStreamingLink() = StreamingLink(\n        id = id.require(),\n        url = url,\n        subs = subs,\n        dubs = dubs,\n        streamer = streamer?.toStreamer()\n    )\n\n    fun NetworkStreamer.toStreamer() = Streamer(\n        id = id.require(),\n        siteName = siteName\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/MediaUnitMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.presentation.model.media.unit.Chapter\nimport io.github.drumber.kitsune.data.presentation.model.media.unit.Episode\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkChapter\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkEpisode\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkMediaUnit\n\nobject MediaUnitMapper {\n    fun NetworkMediaUnit.toMediaUnit() = when (this) {\n        is NetworkEpisode -> toEpisode()\n        is NetworkChapter -> toChapter()\n    }\n\n    fun NetworkEpisode.toEpisode() = Episode(\n        id = id.require(),\n        description = description,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        number = number,\n        seasonNumber = seasonNumber,\n        relativeNumber = relativeNumber,\n        length = length,\n        airdate = airdate,\n        thumbnail = thumbnail\n    )\n\n    fun NetworkChapter.toChapter() = Chapter(\n        id = id.require(),\n        description = description,\n        titles = titles,\n        canonicalTitle = canonicalTitle,\n        number = number,\n        volumeNumber = volumeNumber,\n        length = length,\n        thumbnail = thumbnail,\n        published = published,\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/ProfileLinksMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.mapper.UserMapper.toUser\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLink\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLinkSite\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLinkSite\n\nobject ProfileLinksMapper {\n    fun NetworkProfileLink.toProfileLink(): ProfileLink = ProfileLink(\n        id = id.require(),\n        url = url,\n        profileLinkSite = profileLinkSite?.toProfileLinkSite(),\n        user = user?.toUser()\n    )\n\n    fun NetworkProfileLinkSite.toProfileLinkSite() = ProfileLinkSite(\n        id = id.require(),\n        name = name\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/UserMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toCharacter\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toLocalCharacter\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toNetworkCharacter\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toMedia\nimport io.github.drumber.kitsune.data.mapper.ProfileLinksMapper.toProfileLink\nimport io.github.drumber.kitsune.data.mapper.UserStatsMapper.toUserStats\nimport io.github.drumber.kitsune.data.presentation.model.user.Favorite\nimport io.github.drumber.kitsune.data.presentation.model.user.FavoriteItem\nimport io.github.drumber.kitsune.data.presentation.model.user.User\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalSfwFilterPreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalTitleLanguagePreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkMedia\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavorite\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavoriteItem\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkRatingSystemPreference\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkSfwFilterPreference\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkTitleLanguagePreference\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\n\nobject UserMapper {\n\n    //********************************************************************************************//\n    // From Network\n    //********************************************************************************************//\n\n    fun NetworkUser.toLocalUser() = LocalUser(\n        id = id.require(),\n        createdAt = createdAt,\n        name = name,\n        slug = slug,\n        email = email,\n        title = title,\n        avatar = avatar,\n        coverImage = coverImage,\n        about = about,\n        location = location,\n        gender = gender,\n        birthday = birthday,\n        waifuOrHusbando = waifuOrHusbando,\n        followersCount = followersCount,\n        followingCount = followingCount,\n        country = country,\n        language = language,\n        timeZone = timeZone,\n        theme = theme,\n        sfwFilter = sfwFilter,\n        ratingSystem = ratingSystem?.toLocalRatingSystemPreference(),\n        sfwFilterPreference = sfwFilterPreference?.toLocalSfwFilterPreference(),\n        titleLanguagePreference = titleLanguagePreference?.toLocalTitleLanguagePreference(),\n        waifu = waifu?.toLocalCharacter()\n    )\n\n    fun NetworkUser.toUser() = User(\n        id = id.require(),\n        createdAt = createdAt,\n        name = name,\n        slug = slug,\n        title = title,\n        avatar = avatar,\n        coverImage = coverImage,\n        about = about,\n        location = location,\n        gender = gender,\n        birthday = birthday,\n        waifuOrHusbando = waifuOrHusbando,\n        followersCount = followersCount,\n        followingCount = followingCount,\n        country = country,\n        language = language,\n        timeZone = timeZone,\n        stats = stats?.map { it.toUserStats() },\n        favorites = favorites?.map { it.toFavorite() },\n        waifu = waifu?.toCharacter(),\n        profileLinks = profileLinks?.map { it.toProfileLink() }\n    )\n\n    fun NetworkRatingSystemPreference.toLocalRatingSystemPreference() = when (this) {\n        NetworkRatingSystemPreference.Advanced -> LocalRatingSystemPreference.Advanced\n        NetworkRatingSystemPreference.Regular -> LocalRatingSystemPreference.Regular\n        NetworkRatingSystemPreference.Simple -> LocalRatingSystemPreference.Simple\n    }\n\n    fun NetworkSfwFilterPreference.toLocalSfwFilterPreference() = when (this) {\n        NetworkSfwFilterPreference.SFW -> LocalSfwFilterPreference.SFW\n        NetworkSfwFilterPreference.NSFW_SOMETIMES -> LocalSfwFilterPreference.NSFW_SOMETIMES\n        NetworkSfwFilterPreference.NSFW_EVERYWHERE -> LocalSfwFilterPreference.NSFW_EVERYWHERE\n    }\n\n    fun NetworkTitleLanguagePreference.toLocalTitleLanguagePreference() = when (this) {\n        NetworkTitleLanguagePreference.Canonical -> LocalTitleLanguagePreference.Canonical\n        NetworkTitleLanguagePreference.Romanized -> LocalTitleLanguagePreference.Romanized\n        NetworkTitleLanguagePreference.English -> LocalTitleLanguagePreference.English\n    }\n\n    fun NetworkFavorite.toFavorite(): Favorite = Favorite(\n        id = id.require(),\n        favRank = favRank,\n        item = item?.toFavoriteItem(),\n        user = user?.toUser()\n    )\n\n    fun NetworkFavoriteItem.toFavoriteItem(): FavoriteItem = when (this) {\n        is NetworkMedia -> toMedia()\n        is NetworkCharacter -> toCharacter()\n        else -> throw IllegalArgumentException(\"Unknown favorite item type: ${this.javaClass.name}\")\n    }\n\n    //********************************************************************************************//\n    // From Local\n    //********************************************************************************************//\n\n    fun LocalUser.toUser() = User(\n        id = id,\n        createdAt = createdAt,\n        name = name,\n        slug = slug,\n        title = title,\n        avatar = avatar,\n        coverImage = coverImage,\n        about = about,\n        location = location,\n        gender = gender,\n        birthday = birthday,\n        waifuOrHusbando = waifuOrHusbando,\n        followersCount = followersCount,\n        followingCount = followingCount,\n        country = country,\n        language = language,\n        timeZone = timeZone,\n        stats = null,\n        favorites = null,\n        waifu = waifu?.toCharacter(),\n        profileLinks = null\n    )\n\n    fun LocalUser.toNetworkUser() = NetworkUser(\n        id = id,\n        createdAt = createdAt,\n        name = name,\n        slug = slug,\n        email = email,\n        title = title,\n        avatar = avatar,\n        coverImage = coverImage,\n        about = about,\n        location = location,\n        gender = gender,\n        birthday = birthday,\n        waifuOrHusbando = waifuOrHusbando,\n        country = country,\n        language = language,\n        timeZone = timeZone,\n        theme = theme,\n        sfwFilter = sfwFilter,\n        ratingSystem = ratingSystem?.toNetworkRatingSystemPreference(),\n        sfwFilterPreference = sfwFilterPreference?.toNetworkSfwFilterPreference(),\n        titleLanguagePreference = titleLanguagePreference?.toNetworkTitleLanguagePreference(),\n        waifu = waifu?.toNetworkCharacter(),\n    )\n\n    fun LocalRatingSystemPreference.toNetworkRatingSystemPreference() = when (this) {\n        LocalRatingSystemPreference.Advanced -> NetworkRatingSystemPreference.Advanced\n        LocalRatingSystemPreference.Regular -> NetworkRatingSystemPreference.Regular\n        LocalRatingSystemPreference.Simple -> NetworkRatingSystemPreference.Simple\n    }\n\n    fun LocalSfwFilterPreference.toNetworkSfwFilterPreference() = when (this) {\n        LocalSfwFilterPreference.SFW -> NetworkSfwFilterPreference.SFW\n        LocalSfwFilterPreference.NSFW_SOMETIMES -> NetworkSfwFilterPreference.NSFW_SOMETIMES\n        LocalSfwFilterPreference.NSFW_EVERYWHERE -> NetworkSfwFilterPreference.NSFW_EVERYWHERE\n    }\n\n    fun LocalTitleLanguagePreference.toNetworkTitleLanguagePreference() = when (this) {\n        LocalTitleLanguagePreference.Canonical -> NetworkTitleLanguagePreference.Canonical\n        LocalTitleLanguagePreference.Romanized -> NetworkTitleLanguagePreference.Romanized\n        LocalTitleLanguagePreference.English -> NetworkTitleLanguagePreference.English\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/mapper/UserStatsMapper.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.AmountConsumedPercentiles\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStats\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStatsData.AmountConsumedData\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStatsData.CategoryBreakdownData\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStatsKind\nimport io.github.drumber.kitsune.data.source.network.user.model.stats.NetworkAmountConsumedPercentiles\nimport io.github.drumber.kitsune.data.source.network.user.model.stats.NetworkUserStats\nimport io.github.drumber.kitsune.data.source.network.user.model.stats.NetworkUserStatsData\nimport io.github.drumber.kitsune.data.source.network.user.model.stats.NetworkUserStatsData.NetworkAmountConsumedData\nimport io.github.drumber.kitsune.data.source.network.user.model.stats.NetworkUserStatsData.NetworkCategoryBreakdownData\nimport io.github.drumber.kitsune.data.source.network.user.model.stats.NetworkUserStatsKind\n\nobject UserStatsMapper {\n    fun NetworkUserStats.toUserStats() = UserStats(\n        id = id.require(),\n        kind = kind?.toUserStatsKind(),\n        statsData = statsData?.toUserStatsData()\n    )\n\n    private fun NetworkUserStatsKind.toUserStatsKind() = when (this) {\n        NetworkUserStatsKind.AnimeActivityHistory -> UserStatsKind.AnimeActivityHistory\n        NetworkUserStatsKind.AnimeAmountConsumed -> UserStatsKind.AnimeAmountConsumed\n        NetworkUserStatsKind.AnimeCategoryBreakdown -> UserStatsKind.AnimeCategoryBreakdown\n        NetworkUserStatsKind.AnimeFavoriteYear -> UserStatsKind.AnimeFavoriteYear\n        NetworkUserStatsKind.MangaActivityHistory -> UserStatsKind.MangaActivityHistory\n        NetworkUserStatsKind.MangaAmountConsumed -> UserStatsKind.MangaAmountConsumed\n        NetworkUserStatsKind.MangaCategoryBreakdown -> UserStatsKind.MangaCategoryBreakdown\n        NetworkUserStatsKind.MangaFavoriteYear -> UserStatsKind.MangaFavoriteYear\n    }\n\n    private fun NetworkUserStatsData.toUserStatsData() = when (this) {\n        is NetworkAmountConsumedData -> this.toAmountConsumedData()\n        is NetworkCategoryBreakdownData -> this.toCategoryBreakdownData()\n    }\n\n    private fun NetworkAmountConsumedData.toAmountConsumedData() = AmountConsumedData(\n        time = time,\n        media = media,\n        units = units,\n        completed = completed,\n        percentiles = percentiles?.toAmountConsumedPercentiles(),\n        averageDiffs = averageDiffs?.toAmountConsumedPercentiles()\n    )\n\n    private fun NetworkCategoryBreakdownData.toCategoryBreakdownData() = CategoryBreakdownData(\n        total = total,\n        categories = categories\n    )\n\n    private fun NetworkAmountConsumedPercentiles.toAmountConsumedPercentiles() = AmountConsumedPercentiles(\n        media = media,\n        units = units,\n        time = time\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/LocalRatingSystemPreference.kt",
    "content": "package io.github.drumber.kitsune.data.presentation\n\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\n\nfun LocalRatingSystemPreference.getStringRes() = when (this) {\n    LocalRatingSystemPreference.Simple -> R.string.preference_rating_system_simple\n    LocalRatingSystemPreference.Regular -> R.string.preference_rating_system_regular\n    LocalRatingSystemPreference.Advanced -> R.string.preference_rating_system_advanced\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/dto/CharacterDto.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.dto\n\nimport android.os.Parcelable\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class CharacterDto(\n    val id: String,\n    val slug: String?,\n    val name: String?,\n    val names: Titles?,\n    val otherNames: List<String>?,\n    val malId: Int?,\n    val description: String?,\n    val image: ImageDto?\n) : Parcelable\n\nfun Character.toCharacterDto() = CharacterDto(\n    id = id,\n    slug = slug,\n    name = name,\n    names = names,\n    otherNames = otherNames,\n    malId = malId,\n    description = description,\n    image = image?.toImageDto()\n)\n\nfun CharacterDto.toCharacter() = Character(\n    id = id,\n    slug = slug,\n    name = name,\n    names = names,\n    otherNames = otherNames,\n    malId = malId,\n    description = description,\n    image = image?.toImage(),\n    mediaCharacters = null\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/dto/ImageDto.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.dto\n\nimport android.os.Parcelable\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.ImageDimension\nimport io.github.drumber.kitsune.data.common.ImageDimensions\nimport io.github.drumber.kitsune.data.common.ImageMeta\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class ImageDto(\n    val tiny: String?,\n    val small: String?,\n    val medium: String?,\n    val large: String?,\n    val original: String?,\n    val dimensions: ImageDimensionsDto?\n) : Parcelable\n\n@Parcelize\ndata class ImageDimensionsDto(\n    val tiny: ImageDimensionDto?,\n    val small: ImageDimensionDto?,\n    val medium: ImageDimensionDto?,\n    val large: ImageDimensionDto?\n) : Parcelable\n\n@Parcelize\ndata class ImageDimensionDto(val width: Int?, val height: Int?) : Parcelable\n\nfun Image.toImageDto() = ImageDto(\n    tiny = tiny,\n    small = small,\n    medium = medium,\n    large = large,\n    original = original,\n    dimensions = meta?.dimensions?.toImageDimensionsDto()\n)\n\nfun ImageDimensions.toImageDimensionsDto() = ImageDimensionsDto(\n    tiny = tiny?.toImageDimensionDto(),\n    small = small?.toImageDimensionDto(),\n    medium = medium?.toImageDimensionDto(),\n    large = large?.toImageDimensionDto()\n)\n\nfun ImageDimension.toImageDimensionDto() = ImageDimensionDto(\n    width = width,\n    height = height\n)\n\nfun ImageDto.toImage() = Image(\n    tiny = tiny,\n    small = small,\n    medium = medium,\n    large = large,\n    original = original,\n    meta = ImageMeta(dimensions?.toImageDimensions())\n)\n\nfun ImageDimensionsDto.toImageDimensions() = ImageDimensions(\n    tiny = tiny?.toImageDimension(),\n    small = small?.toImageDimension(),\n    medium = medium?.toImageDimension(),\n    large = large?.toImageDimension()\n)\n\nfun ImageDimensionDto.toImageDimension() = ImageDimension(\n    width = width,\n    height = height\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/dto/MediaDto.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.dto\n\nimport android.os.Parcelable\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.common.media.ReleaseStatus\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class MediaDto(\n    val id: String,\n    val type: MediaType,\n    val slug: String?,\n\n    val description: String?,\n    val titles: Titles?,\n    val canonicalTitle: String?,\n    val abbreviatedTitles: List<String>?,\n\n    val averageRating: String?,\n    val popularityRank: Int?,\n    val ratingRank: Int?,\n\n    val startDate: String?,\n    val endDate: String?,\n    val nextRelease: String?,\n    val tba: String?,\n    val status: ReleaseStatus?,\n\n    val ageRating: AgeRating?,\n    val ageRatingGuide: String?,\n\n    val posterImage: ImageDto?,\n    val coverImage: ImageDto?,\n\n    val totalLength: Int?\n) : Parcelable\n\nfun Media.toMediaDto() = MediaDto(\n    id = id,\n    type = when (this) {\n        is Anime -> MediaType.Anime\n        is Manga -> MediaType.Manga\n    },\n    slug = slug,\n    description = description,\n    titles = titles,\n    canonicalTitle = canonicalTitle,\n    abbreviatedTitles = abbreviatedTitles,\n    averageRating = averageRating,\n    popularityRank = popularityRank,\n    ratingRank = ratingRank,\n    startDate = startDate,\n    endDate = endDate,\n    nextRelease = nextRelease,\n    tba = tba,\n    status = status,\n    ageRating = ageRating,\n    ageRatingGuide = ageRatingGuide,\n    posterImage = posterImage?.toImageDto(),\n    coverImage = coverImage?.toImageDto(),\n    totalLength = totalLength\n)\n\nfun MediaDto.toMedia(): Media {\n    return when (type) {\n        MediaType.Anime -> toAnime()\n        MediaType.Manga -> toManga()\n    }\n}\n\nprivate fun MediaDto.toAnime() = Anime(\n    id = id,\n    slug = slug,\n    description = description,\n    titles = titles,\n    canonicalTitle = canonicalTitle,\n    abbreviatedTitles = abbreviatedTitles,\n    averageRating = averageRating,\n    ratingFrequencies = null,\n    userCount = null,\n    favoritesCount = null,\n    popularityRank = popularityRank,\n    ratingRank = ratingRank,\n    startDate = startDate,\n    endDate = endDate,\n    nextRelease = nextRelease,\n    tba = tba,\n    status = status,\n    ageRating = ageRating,\n    ageRatingGuide = ageRatingGuide,\n    nsfw = null,\n    posterImage = posterImage?.toImage(),\n    coverImage = coverImage?.toImage(),\n    totalLength = totalLength,\n    episodeCount = null,\n    episodeLength = null,\n    youtubeVideoId = null,\n    subtype = null,\n    categories = null,\n    animeProduction = null,\n    streamingLinks = null,\n    mediaRelationships = null\n)\n\nprivate fun MediaDto.toManga() = Manga(\n    id = id,\n    slug = slug,\n    description = description,\n    titles = titles,\n    canonicalTitle = canonicalTitle,\n    abbreviatedTitles = abbreviatedTitles,\n    averageRating = averageRating,\n    ratingFrequencies = null,\n    userCount = null,\n    favoritesCount = null,\n    popularityRank = popularityRank,\n    ratingRank = ratingRank,\n    startDate = startDate,\n    endDate = endDate,\n    nextRelease = nextRelease,\n    tba = tba,\n    status = status,\n    ageRating = ageRating,\n    ageRatingGuide = ageRatingGuide,\n    nsfw = null,\n    posterImage = posterImage?.toImage(),\n    coverImage = coverImage?.toImage(),\n    totalLength = totalLength,\n    chapterCount = null,\n    volumeCount = null,\n    subtype = null,\n    serialization = null,\n    categories = null,\n    mediaRelationships = null\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/dto/MediaUnitDto.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.dto\n\nimport android.os.Parcelable\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.presentation.dto.MediaUnitDto.UnitType\nimport io.github.drumber.kitsune.data.presentation.model.media.unit.Chapter\nimport io.github.drumber.kitsune.data.presentation.model.media.unit.Episode\nimport io.github.drumber.kitsune.data.presentation.model.media.unit.MediaUnit\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class MediaUnitDto(\n    val id: String,\n    val type: UnitType,\n    val description: String?,\n    val titles: Titles?,\n    val canonicalTitle: String?,\n    val number: Int?,\n    val length: String?,\n    val thumbnail: ImageDto?,\n\n    // Episode specific attributes\n    val seasonNumber: Int?,\n    val relativeNumber: Int?,\n    val airdate: String?,\n\n    // Chapter specific attributes\n    val volumeNumber: Int?,\n    val published: String?\n) : Parcelable {\n    enum class UnitType {\n        Episode,\n        Chapter\n    }\n}\n\nfun MediaUnit.toMediaUnitDto() = when (this) {\n    is Episode -> toMediaUnitDto()\n    is Chapter -> toMediaUnitDto()\n}\n\nfun MediaUnitDto.toMediaUnit() = when (type) {\n    UnitType.Episode -> toEpisode()\n    UnitType.Chapter -> toChapter()\n}\n\nprivate fun Episode.toMediaUnitDto() = MediaUnitDto(\n    id = id,\n    type = UnitType.Episode,\n    description = description,\n    titles = titles,\n    canonicalTitle = canonicalTitle,\n    number = number,\n    length = length,\n    thumbnail = thumbnail?.toImageDto(),\n    seasonNumber = seasonNumber,\n    relativeNumber = relativeNumber,\n    airdate = airdate,\n    volumeNumber = null,\n    published = null\n)\n\nprivate fun Chapter.toMediaUnitDto() = MediaUnitDto(\n    id = id,\n    type = UnitType.Chapter,\n    description = description,\n    titles = titles,\n    canonicalTitle = canonicalTitle,\n    number = number,\n    length = length,\n    thumbnail = thumbnail?.toImageDto(),\n    seasonNumber = null,\n    relativeNumber = null,\n    airdate = null,\n    volumeNumber = volumeNumber,\n    published = published\n)\n\nprivate fun MediaUnitDto.toEpisode() = Episode(\n    id = id,\n    description = description,\n    titles = titles,\n    canonicalTitle = canonicalTitle,\n    number = number,\n    length = length,\n    thumbnail = thumbnail?.toImage(),\n    seasonNumber = seasonNumber,\n    relativeNumber = relativeNumber,\n    airdate = airdate\n)\n\nprivate fun MediaUnitDto.toChapter() = Chapter(\n    id = id,\n    description = description,\n    titles = titles,\n    canonicalTitle = canonicalTitle,\n    number = number,\n    length = length,\n    thumbnail = thumbnail?.toImage(),\n    volumeNumber = volumeNumber,\n    published = published\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/algolia/AlgoliaKey.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.algolia\n\ndata class AlgoliaKey(\n    val key: String,\n    val index: String?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/algolia/AlgoliaKeyCollection.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.algolia\n\ndata class AlgoliaKeyCollection(\n    val users: AlgoliaKey?,\n    val posts: AlgoliaKey?,\n    val media: AlgoliaKey?,\n    val groups: AlgoliaKey?,\n    val characters: AlgoliaKey?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/algolia/SearchType.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.algolia\n\nenum class SearchType(private val algoliaKey: (AlgoliaKeyCollection) -> AlgoliaKey?) {\n    Users({ it.users }),\n    Posts({ it.posts }),\n    Media({ it.media }),\n    Groups({ it.groups }),\n    Characters({ it.characters });\n\n    fun getAlgoliaKey(algoliaKeyCollection: AlgoliaKeyCollection) = algoliaKey(algoliaKeyCollection)\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/appupdate/AppRelease.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.appupdate\n\ndata class AppRelease(\n    val version: String,\n    val url: String\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/appupdate/UpdateCheckResult.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.appupdate\n\nsealed class UpdateCheckResult {\n    data object NoNewVersion : UpdateCheckResult()\n    data class NewVersion(val release: AppRelease) : UpdateCheckResult()\n    data class Error(val exception: Exception) : UpdateCheckResult()\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/character/Character.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.character\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.presentation.model.user.FavoriteItem\n\ndata class Character(\n    val id: String,\n    val slug: String?,\n    val name: String?,\n    val names: Titles?,\n    val otherNames: List<String>?,\n    val malId: Int?,\n    val description: String?,\n    val image: Image?,\n\n    val mediaCharacters: List<MediaCharacter>?\n) : FavoriteItem\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/character/CharacterSearchResult.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.character\n\nimport io.github.drumber.kitsune.data.common.Image\n\ndata class CharacterSearchResult(\n    val id: String,\n    val slug: String?,\n    val name: String?,\n    val image: Image?,\n    val primaryMediaTitle: String?\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/character/MediaCharacter.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.character\n\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\n\ndata class MediaCharacter(\n    val id: String,\n    val role: MediaCharacterRole?,\n    val media: Media?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/character/MediaCharacterRole.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.character\n\nimport androidx.annotation.StringRes\nimport io.github.drumber.kitsune.R\n\nenum class MediaCharacterRole {\n    MAIN,\n    SUPPORTING,\n    RECURRING,\n    CAMEO\n}\n\n@StringRes\nfun MediaCharacterRole.getStringRes() = when (this) {\n    MediaCharacterRole.MAIN -> R.string.character_role_main\n    MediaCharacterRole.SUPPORTING -> R.string.character_role_supporting\n    MediaCharacterRole.RECURRING -> R.string.character_role_recurring\n    MediaCharacterRole.CAMEO -> R.string.character_role_cameo\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryEntry.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\n\ndata class LibraryEntry(\n    val id: String,\n    val updatedAt: String?,\n\n    val startedAt: String?,\n    val finishedAt: String?,\n    val progressedAt: String?,\n\n    val status: LibraryStatus?,\n    val progress: Int?,\n    val reconsuming: Boolean?,\n    val reconsumeCount: Int?,\n    val volumesOwned: Int?,\n    val ratingTwenty: Int?,\n\n    val notes: String?,\n    val privateEntry: Boolean?,\n    val reactionSkipped: ReactionSkip?,\n\n    val media: Media?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryEntryFilter.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.library.LibraryEntryKind\n\ndata class LibraryEntryFilter(\n    val kind: LibraryEntryKind,\n    val libraryStatus: List<LibraryStatus>,\n    private val initialFilter: Filter = Filter()\n) {\n\n    fun pageSize(pageSize: Int): LibraryEntryFilter {\n        return copy(initialFilter = Filter(initialFilter.options.toMutableMap()).pageLimit(pageSize))\n    }\n\n    fun buildFilter() = Filter(initialFilter.options.toMutableMap()).apply {\n            if (kind != LibraryEntryKind.All) {\n                filter(\"kind\", kind.name.lowercase())\n            }\n            if (libraryStatus.isNotEmpty()) {\n                val status = libraryStatus.joinToString(\",\") { it.getFilterValue() }\n                filter(\"status\", status)\n            }\n        }\n\n    fun isFiltered() = kind != LibraryEntryKind.All || libraryStatus.isNotEmpty()\n\n    /** Checks if the initial filter has a 'title' filter applied. */\n    fun isFilteredBySearchQuery() = initialFilter.hasFilterAttribute(\"title\")\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryEntryModification.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\ndata class LibraryEntryModification(\n    val id: String,\n\n    val createTime: Long = System.currentTimeMillis(),\n    val state: LibraryModificationState = LibraryModificationState.NOT_SYNCHRONIZED,\n\n    val startedAt: String?,\n    val finishedAt: String?,\n\n    val status: LibraryStatus?,\n    val progress: Int?,\n    val reconsumeCount: Int?,\n    val volumesOwned: Int?,\n    /**  Set to `-1` to remove rating (will be mapped to `null` by the json serializer) */\n    val ratingTwenty: Int?,\n\n    val notes: String?,\n    val privateEntry: Boolean?\n) {\n\n    companion object {\n        fun withIdAndNulls(id: String) = LibraryEntryModification(\n            id = id,\n            startedAt = null,\n            finishedAt = null,\n            status = null,\n            progress = null,\n            reconsumeCount = null,\n            volumesOwned = null,\n            ratingTwenty = null,\n            notes = null,\n            privateEntry = null\n        )\n    }\n\n    /**\n     * Apply modifications to the specified library entry.\n     *\n     * @param ignoreBlankNotes\n     * Do not apply notes if modified notes is blank and library entry notes is null.\n     *\n     * @return the given library entry for convenience.\n     */\n    fun applyToLibraryEntry(\n        libraryEntry: LibraryEntry, ignoreBlankNotes: Boolean = true\n    ): LibraryEntry {\n        return libraryEntry.copy(\n            startedAt = startedAt ?: libraryEntry.startedAt,\n            finishedAt = finishedAt ?: libraryEntry.finishedAt,\n            status = status ?: libraryEntry.status,\n            progress = progress ?: libraryEntry.progress,\n            reconsumeCount = reconsumeCount ?: libraryEntry.reconsumeCount,\n            volumesOwned = volumesOwned ?: libraryEntry.volumesOwned,\n            ratingTwenty = ratingTwenty ?: libraryEntry.ratingTwenty,\n            notes = notes\n                ?.takeIf { !ignoreBlankNotes || libraryEntry.notes != null || it.isNotBlank() }\n                ?: libraryEntry.notes,\n            privateEntry = privateEntry ?: libraryEntry.privateEntry\n        )\n    }\n\n    fun mergeModificationFrom(other: LibraryEntryModification) = LibraryEntryModification(\n        id = id,\n        status = other.status ?: status,\n        progress = other.progress ?: progress,\n        volumesOwned = other.volumesOwned ?: volumesOwned,\n        reconsumeCount = other.reconsumeCount ?: reconsumeCount,\n        notes = other.notes ?: notes,\n        privateEntry = other.privateEntry ?: privateEntry,\n        startedAt = other.startedAt ?: startedAt,\n        finishedAt = other.finishedAt ?: finishedAt,\n        ratingTwenty = other.ratingTwenty ?: ratingTwenty\n    )\n\n    fun toLocalLibraryEntry(ignoreBlankNotes: Boolean = true): LibraryEntry {\n        return LibraryEntry(\n            id = id,\n            updatedAt = null,\n            startedAt = startedAt,\n            finishedAt = finishedAt,\n            progressedAt = null,\n            status = status,\n            progress = progress,\n            reconsuming = null,\n            reconsumeCount = reconsumeCount,\n            volumesOwned = volumesOwned,\n            ratingTwenty = ratingTwenty,\n            notes = notes?.takeIf { !ignoreBlankNotes || it.isNotBlank() },\n            privateEntry = privateEntry,\n            reactionSkipped = null,\n            media = null\n        )\n    }\n\n    fun isEqualToLibraryEntry(libraryEntry: LibraryEntry): Boolean {\n        return libraryEntry == applyToLibraryEntry(libraryEntry)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryEntryUiModel.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nsealed class LibraryEntryUiModel {\n    data class StatusSeparatorModel(\n        val status: LibraryStatus,\n        val isMangaSelected: Boolean\n    ) : LibraryEntryUiModel()\n\n    data class EntryModel(val entry: LibraryEntryWithModification) : LibraryEntryUiModel()\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryEntryWithModification.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.formatRatingTwenty\n\ndata class LibraryEntryWithModification(\n    val libraryEntry: LibraryEntry,\n    val modification: LibraryEntryModification?\n) {\n\n    val id\n        get() = libraryEntry.id\n\n    val media\n        get() = libraryEntry.media\n\n    val episodeCount: Int?\n        get() = media?.episodeOrChapterCount\n\n    val hasEpisodesCount: Boolean\n        get() = episodeCount != null\n\n    val episodeCountFormatted: String\n        get() = episodeCount?.toString() ?: \"∞\"\n\n    val progress\n        get() = modification?.progress ?: libraryEntry.progress\n\n    val hasStartedWatching: Boolean\n        get() = progress?.equals(0) == false\n\n    val hasStartedWatchingOrIsCurrent: Boolean\n        get() = hasStartedWatching || status == LibraryStatus.Current\n\n    val canWatchEpisode: Boolean\n        get() = progress != episodeCount\n\n    val volumesOwned\n        get() = modification?.volumesOwned ?: libraryEntry.volumesOwned\n\n    val ratingTwenty\n        get() = modification?.ratingTwenty ?: libraryEntry.ratingTwenty\n\n    val ratingFormatted: String?\n        get() = ratingTwenty?.let {\n            when {\n                it == -1 -> null\n                else -> it.formatRatingTwenty()\n            }\n        }\n\n    val hasRating: Boolean\n        get() = ratingTwenty != null\n\n    val status\n        get() = modification?.status ?: libraryEntry.status\n\n    val reconsumeCount\n        get() = modification?.reconsumeCount ?: libraryEntry.reconsumeCount\n\n    val isPrivate\n        get() = modification?.privateEntry ?: libraryEntry.privateEntry\n\n    val startedAt\n        get() = modification?.startedAt ?: libraryEntry.startedAt\n\n    val finishedAt\n        get() = modification?.finishedAt ?: libraryEntry.finishedAt\n\n    val notes\n        get() = modification?.notes ?: libraryEntry.notes\n\n    val isSynchronizing\n        get() = modification?.state == LibraryModificationState.SYNCHRONIZING\n\n    val isNotSynced\n        get() = !isSynchronizing &&\n                modification != null &&\n                !modification.isEqualToLibraryEntry(libraryEntry)\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryModificationState.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nenum class LibraryModificationState {\n    SYNCHRONIZING,\n    NOT_SYNCHRONIZED\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryStatus.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nimport io.github.drumber.kitsune.R\n\nenum class LibraryStatus {\n    Current,\n    Planned,\n    Completed,\n    OnHold,\n    Dropped\n}\n\nfun LibraryStatus.getStringResId(isAnime: Boolean = true) = when (this) {\n    LibraryStatus.Completed -> R.string.library_status_completed\n    LibraryStatus.Current -> if (isAnime) R.string.library_status_watching else R.string.library_status_reading\n    LibraryStatus.Dropped -> R.string.library_status_dropped\n    LibraryStatus.OnHold -> R.string.library_status_on_hold\n    LibraryStatus.Planned -> if (isAnime) R.string.library_status_planned else R.string.library_status_planned_manga\n}\n\nfun LibraryStatus.getFilterValue() = when (this) {\n    LibraryStatus.Current -> \"current\"\n    LibraryStatus.Planned -> \"planned\"\n    LibraryStatus.Completed -> \"completed\"\n    LibraryStatus.OnHold -> \"on_hold\"\n    LibraryStatus.Dropped -> \"dropped\"\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/library/ReactionSkip.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nenum class ReactionSkip {\n    Unskipped,\n    Skipped,\n    Ignored\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/mapping/Mapping.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.mapping\n\nimport io.github.drumber.kitsune.constants.Kitsu\n\ndata class Mapping(\n    val id: String,\n    val externalSite: String?,\n    val externalId: String?\n)\n\nfun Mapping.getSiteName() = when (externalSite) {\n    \"kitsu/anime\", \"kitsu/manga\" -> \"Kitsu\"\n    \"anidb\" -> \"AniDB\"\n    \"anilist\", \"anilist/anime\", \"anilist/manga\" -> \"AniList\"\n    \"myanimelist\", \"myanimelist/anime\", \"myanimelist/manga\" -> \"MyAnimeList\"\n    \"thetvdb\", \"thetvdb/season\", \"thetvdb/series\" -> \"TheTVDB\"\n    \"trakt\" -> \"Trakt\"\n    \"hulu\" -> \"Hulu\"\n    \"mangaupdates\" -> \"Baka-Updates Manga\"\n    else -> null\n}\n\nfun Mapping.getExternalUrl() = when (externalSite) {\n    \"kitsu/anime\" -> \"${Kitsu.BASE_URL}/anime/$externalId\"\n    \"kitsu/manga\" -> \"${Kitsu.BASE_URL}/manga/$externalId\"\n    \"anidb\" -> \"https://anidb.net/anime/$externalId\"\n    \"anilist/anime\" -> \"https://anilist.co/anime/$externalId\"\n    \"anilist/manga\" -> \"https://anilist.co/manga/$externalId\"\n    \"myanimelist/anime\" -> \"https://myanimelist.net/anime/$externalId\"\n    \"myanimelist/manga\" -> \"https://myanimelist.net/manga/$externalId\"\n    \"thetvdb/season\" -> \"https://thetvdb.com/dereferrer/season/$externalId\"\n    \"thetvdb/series\" -> \"https://thetvdb.com/dereferrer/series/$externalId\"\n    \"thetvdb\" -> \"https://thetvdb.com/dereferrer/series/${externalId?.replace(Regex(\"/.*\"), \"\")}\"\n    \"trakt\" -> \"https://trakt.tv/shows/$externalId\"\n    \"hulu\" -> \"https://hulu.jp/series/$externalId\"\n    \"mangaupdates\" -> \"https://www.mangaupdates.com/series/$externalId\"\n    else -> null\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/Anime.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.common.media.AnimeSubtype\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.common.media.RatingFrequencies\nimport io.github.drumber.kitsune.data.common.media.ReleaseStatus\nimport io.github.drumber.kitsune.data.presentation.model.media.category.Category\nimport io.github.drumber.kitsune.data.presentation.model.media.production.AnimeProduction\nimport io.github.drumber.kitsune.data.presentation.model.media.relationship.MediaRelationship\nimport io.github.drumber.kitsune.data.presentation.model.media.streamer.StreamingLink\n\ndata class Anime(\n    override val id: String,\n    override val slug: String?,\n\n    override val description: String?,\n    override val titles: Titles?,\n    override val canonicalTitle: String?,\n    override val abbreviatedTitles: List<String>?,\n\n    override val averageRating: String?,\n    override val ratingFrequencies: RatingFrequencies?,\n    override val userCount: Int?,\n    override val favoritesCount: Int?,\n    override val popularityRank: Int?,\n    override val ratingRank: Int?,\n\n    override val startDate: String?,\n    override val endDate: String?,\n    override val nextRelease: String?,\n    override val tba: String?,\n    override val status: ReleaseStatus?,\n\n    override val ageRating: AgeRating?,\n    override val ageRatingGuide: String?,\n    override val nsfw: Boolean?,\n\n    override val posterImage: Image?,\n    override val coverImage: Image?,\n\n    override val totalLength: Int?,\n    val episodeCount: Int?,\n    val episodeLength: Int?,\n    val youtubeVideoId: String?,\n    val subtype: AnimeSubtype?,\n\n    override val categories: List<Category>?,\n    val animeProduction: List<AnimeProduction>?,\n    val streamingLinks: List<StreamingLink>?,\n    override val mediaRelationships: List<MediaRelationship>?\n) : Media() {\n\n    override val mediaType: MediaType\n        get() = MediaType.Anime\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/Manga.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.common.media.MangaSubtype\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.common.media.RatingFrequencies\nimport io.github.drumber.kitsune.data.common.media.ReleaseStatus\nimport io.github.drumber.kitsune.data.presentation.model.media.category.Category\nimport io.github.drumber.kitsune.data.presentation.model.media.relationship.MediaRelationship\n\ndata class Manga(\n    override val id: String,\n    override val slug: String?,\n\n    override val description: String?,\n    override val titles: Titles?,\n    override val canonicalTitle: String?,\n    override val abbreviatedTitles: List<String>?,\n\n    override val averageRating: String?,\n    override val ratingFrequencies: RatingFrequencies?,\n    override val userCount: Int?,\n    override val favoritesCount: Int?,\n    override val popularityRank: Int?,\n    override val ratingRank: Int?,\n\n    override val startDate: String?,\n    override val endDate: String?,\n    override val nextRelease: String?,\n    override val tba: String?,\n    override val status: ReleaseStatus?,\n\n    override val ageRating: AgeRating?,\n    override val ageRatingGuide: String?,\n    override val nsfw: Boolean?,\n\n    override val posterImage: Image?,\n    override val coverImage: Image?,\n\n    override val totalLength: Int?,\n    val chapterCount: Int?,\n    val volumeCount: Int?,\n    val subtype: MangaSubtype?,\n    val serialization: String?,\n\n    override val categories: List<Category>?,\n    override val mediaRelationships: List<MediaRelationship>?\n) : Media() {\n\n    override val mediaType: MediaType\n        get() = MediaType.Manga\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/Media.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media\n\nimport android.content.Context\nimport androidx.annotation.StringRes\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.en\nimport io.github.drumber.kitsune.data.common.enJp\nimport io.github.drumber.kitsune.data.common.jaJp\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.common.media.RatingFrequencies\nimport io.github.drumber.kitsune.data.common.media.ReleaseStatus\nimport io.github.drumber.kitsune.data.presentation.model.media.category.Category\nimport io.github.drumber.kitsune.data.presentation.model.media.production.AnimeProductionRole\nimport io.github.drumber.kitsune.data.presentation.model.media.relationship.MediaRelationship\nimport io.github.drumber.kitsune.data.presentation.model.user.FavoriteItem\nimport io.github.drumber.kitsune.util.DataUtil\nimport io.github.drumber.kitsune.util.TimeUtil\nimport io.github.drumber.kitsune.util.extensions.format\nimport io.github.drumber.kitsune.util.formatDate\nimport io.github.drumber.kitsune.util.parseDate\nimport io.github.drumber.kitsune.util.toCalendar\nimport java.util.Calendar\n\nsealed class Media : FavoriteItem {\n    abstract val id: String\n    abstract val slug: String?\n\n    abstract val description: String?\n    abstract val titles: Titles?\n    abstract val canonicalTitle: String?\n    abstract val abbreviatedTitles: List<String>?\n\n    abstract val averageRating: String?\n    abstract val ratingFrequencies: RatingFrequencies?\n    abstract val userCount: Int?\n    abstract val favoritesCount: Int?\n    abstract val popularityRank: Int?\n    abstract val ratingRank: Int?\n\n    abstract val startDate: String?\n    abstract val endDate: String?\n    abstract val nextRelease: String?\n    abstract val tba: String?\n    abstract val status: ReleaseStatus?\n\n    abstract val ageRating: AgeRating?\n    abstract val ageRatingGuide: String?\n    abstract val nsfw: Boolean?\n\n    abstract val posterImage: Image?\n    abstract val coverImage: Image?\n\n    abstract val totalLength: Int?\n\n    abstract val categories: List<Category>?\n    abstract val mediaRelationships: List<MediaRelationship>?\n\n    //********************************************************************************************//\n\n    abstract val mediaType: MediaType\n\n    val title get() = DataUtil.getTitle(titles, canonicalTitle)\n\n    val titleEn get() = titles?.en\n\n    val titleEnJp get() = titles?.enJp\n\n    val titleJaJp get() = titles?.jaJp\n\n    val abbreviatedTitlesFormatted get() = abbreviatedTitles?.joinToString(\", \")\n\n    val avgRatingFormatted get() = averageRating?.tryFormatDouble()\n\n    val posterImageUrl get() = posterImage?.smallOrHigher()\n\n    val coverImageUrl get() = coverImage?.originalOrDown()\n\n    val subtypeFormatted\n        get() = when (this) {\n            is Anime -> subtype\n            is Manga -> subtype\n        }?.name.orEmpty().replaceFirstChar(Char::titlecase)\n\n    val publishingYear: Int?\n        get() = startDate?.takeIf { it.isNotBlank() }\n            ?.parseDate()?.toCalendar()\n            ?.get(Calendar.YEAR)\n\n    fun publishingYearText(context: Context): String {\n        val publishingYear = publishingYear\n        return when {\n            publishingYear != null -> publishingYear.toString()\n            status == ReleaseStatus.TBA -> context.getString(R.string.status_tba)\n            else -> \"-\"\n        }\n    }\n\n    @get:StringRes\n    val seasonStringRes: Int\n        get() {\n            val date = startDate?.parseDate()?.toCalendar()\n            return when (date?.get(Calendar.MONTH)?.plus(1)) {\n                12, 1, 2 -> R.string.season_winter\n                in 3..5 -> R.string.season_spring\n                in 6..8 -> R.string.season_summer\n                in 9..11 -> R.string.season_fall\n                else -> R.string.no_information\n            }\n        }\n\n    val seasonYear: String\n        get() = startDate?.parseDate()?.toCalendar()?.let { date ->\n            val year = date.get(Calendar.YEAR)\n            val month = date.get(Calendar.MONTH) + 1\n            if (month == 12) {\n                year + 1\n            } else {\n                year\n            }\n        }?.toString() ?: \"\"\n\n    val airedText: String\n        get() {\n            var airedText = formatDate(startDate)\n            if (!endDate.isNullOrBlank() && startDate != endDate) {\n                airedText += \" - ${formatDate(endDate)}\"\n            }\n            return airedText\n        }\n\n    @get:StringRes\n    val statusStringRes: Int\n        get() = when (status) {\n            ReleaseStatus.Current -> if (this is Anime) R.string.status_current else R.string.status_current_manga\n            ReleaseStatus.Finished -> R.string.status_finished\n            ReleaseStatus.TBA -> R.string.status_tba\n            ReleaseStatus.Unreleased -> R.string.status_unreleased\n            ReleaseStatus.Upcoming -> R.string.status_upcoming\n            null -> R.string.no_information\n        }\n\n    val ageRatingText: String?\n        get() {\n            var ageRatingText = ageRating?.name ?: return null\n            if (ageRatingGuide != null) {\n                ageRatingText += \" - $ageRatingGuide\"\n            }\n            return ageRatingText\n        }\n\n    val serializationText: String? get() = (this as? Manga)?.serialization\n\n    val chapters: String? get() = (this as? Manga)?.chapterCount?.toString()\n\n    val volumes: String?\n        get() = (this as? Manga)?.volumeCount?.let { volumes ->\n            if (volumes > 0) {\n                volumes.toString()\n            } else {\n                null\n            }\n        }\n\n    val episodes: String? get() = (this as? Anime)?.episodeCount?.toString()\n\n    val episodeOrChapterCount\n        get() = (this as? Anime)?.episodeCount ?: (this as? Manga)?.chapterCount\n\n    fun lengthText(context: Context): String? {\n        if (this is Anime) {\n            val count = episodeCount\n            val length = episodeLength ?: return null\n            val lengthEachText = context.getString(R.string.data_length_each, length)\n            return if (count == null) {\n                lengthEachText\n            } else {\n                val minutes = count * length.toLong()\n                val durationText = TimeUtil.timeToHumanReadableFormat(minutes * 60, context)\n                if (count > 1) {\n                    context.getString(\n                        R.string.data_length_total,\n                        durationText\n                    ) + \" ($lengthEachText)\"\n                } else {\n                    durationText\n                }\n            }\n        }\n        return null\n    }\n\n    val trailerUrl: String?\n        get() = if (this is Anime && !youtubeVideoId.isNullOrBlank()) {\n            \"https://www.youtube.com/watch?v=$youtubeVideoId\"\n        } else null\n\n    val trailerCoverUrl: String?\n        get() = if (this is Anime && !youtubeVideoId.isNullOrBlank()) {\n            \"https://img.youtube.com/vi/$youtubeVideoId/mqdefault.jpg\"\n        } else null\n\n    fun getProducer(role: AnimeProductionRole): String? {\n        return (this as? Anime)?.animeProduction?.filter { it.role == role }\n            ?.mapNotNull { it.producer?.name }\n            ?.distinct()\n            ?.joinToString(\", \")\n    }\n\n    fun hasStreamingLinks() = this is Anime && !streamingLinks.isNullOrEmpty()\n\n    fun hasMediaRelationships() = !mediaRelationships.isNullOrEmpty()\n\n    fun hasRatingFrequencies() = ratingFrequencies != null\n\n    private fun formatDate(dateString: String?): String {\n        return dateString?.takeIf { it.isNotBlank() }?.parseDate()?.formatDate() ?: \"\"\n    }\n\n    private fun String?.tryFormatDouble() = this?.toDoubleOrNull()?.format()\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/MediaSelector.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media\n\nimport android.os.Parcelable\nimport io.github.drumber.kitsune.data.common.FilterOptions\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class MediaSelector(\n    val mediaType: MediaType,\n    val filterOptions: FilterOptions,\n    val requestType: RequestType = RequestType.ALL\n) : Parcelable\n\nval MediaType.identifier\n    get() = when (this) {\n        MediaType.Anime -> \"anime\"\n        MediaType.Manga -> \"manga\"\n    }\n\nenum class RequestType {\n    ALL,\n    TRENDING\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/category/Category.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.category\n\ndata class Category(\n    val id: String,\n    val slug: String?,\n\n    val title: String?,\n    val description: String?,\n    val nsfw: Boolean?,\n\n    val totalMediaCount: Int?,\n    val childCount: Int?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/category/CategoryNode.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.category\n\ndata class CategoryNode(\n    val category: Category,\n    val childCategories: MutableList<CategoryNode> = mutableListOf()\n) {\n\n    fun hasChildren() = category.childCount?.equals(0) == false\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/production/AnimeProduction.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.production\n\ndata class AnimeProduction(\n    val id: String,\n    val role: AnimeProductionRole?,\n\n    val producer: Producer?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/production/AnimeProductionRole.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.production\n\nenum class AnimeProductionRole {\n    Licensor,\n    Producer,\n    Studio\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/production/Casting.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.production\n\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\n\ndata class Casting(\n    val id: String,\n    val role: String?,\n    val voiceActor: Boolean?,\n    val featured: Boolean?,\n    val language: String?,\n\n    val character: Character?,\n    val person: Person?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/production/Person.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.production\n\nimport io.github.drumber.kitsune.data.common.Image\n\ndata class Person(\n    val id: String,\n    val name: String?,\n    val description: String?,\n    val image: Image?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/production/Producer.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.production\n\ndata class Producer(\n    val id: String,\n    val slug: String?,\n    val name: String?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/relationship/MediaRelationship.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.relationship\n\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\n\ndata class MediaRelationship(\n    val id: String,\n    val role: MediaRelationshipRole?,\n\n    val media: Media?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/relationship/MediaRelationshipRole.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.relationship\n\nimport androidx.annotation.StringRes\nimport io.github.drumber.kitsune.R\n\n/**\n * Media relationship roles, sort order from\n * https://github.com/hummingbird-me/kitsu-server/blob/the-future/app/models/media_relationship.rb\n */\nenum class MediaRelationshipRole {\n    Sequel,\n    Prequel,\n    AlternativeSetting,\n    AlternativeVersion,\n    SideStory,\n    ParentStory,\n    Summary,\n    FullStory,\n    Spinoff,\n    Adaptation,\n    Character,\n    Other\n}\n\n@StringRes\nfun MediaRelationshipRole.getStringRes(): Int {\n    return when (this) {\n        MediaRelationshipRole.Sequel -> R.string.relationship_sequel\n        MediaRelationshipRole.Prequel -> R.string.relationship_prequel\n        MediaRelationshipRole.AlternativeSetting -> R.string.relationship_alternative_setting\n        MediaRelationshipRole.AlternativeVersion -> R.string.relationship_alternative_version\n        MediaRelationshipRole.SideStory -> R.string.relationship_side_story\n        MediaRelationshipRole.ParentStory -> R.string.relationship_parent_story\n        MediaRelationshipRole.Summary -> R.string.relationship_summary\n        MediaRelationshipRole.FullStory -> R.string.relationship_full_story\n        MediaRelationshipRole.Spinoff -> R.string.relationship_spinoff\n        MediaRelationshipRole.Adaptation -> R.string.relationship_adaptation\n        MediaRelationshipRole.Character -> R.string.relationship_character\n        MediaRelationshipRole.Other -> R.string.relationship_other\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/streamer/Streamer.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.streamer\n\ndata class Streamer(\n    val id: String,\n    val siteName: String?,\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/streamer/StreamingLink.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.streamer\n\ndata class StreamingLink(\n    val id: String,\n    val url: String?,\n    val subs: List<String>?,\n    val dubs: List<String>?,\n\n    val streamer: Streamer?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/unit/Chapter.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.unit\n\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\n\ndata class Chapter(\n    override val id: String,\n\n    override val description: String?,\n    override val titles: Titles?,\n    override val canonicalTitle: String?,\n\n    override val number: Int?,\n    val volumeNumber: Int?,\n    override val length: String?,\n\n    override val thumbnail: Image?,\n    val published: String?\n) : MediaUnit {\n\n    override val numberStringRes\n        get() = R.string.unit_chapter\n\n    override val lengthStringRes: Int\n        get() = R.plurals.unit_pages\n\n    override val date: String?\n        get() = published\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/unit/Episode.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.unit\n\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\n\ndata class Episode(\n    override val id: String,\n\n    override val description: String?,\n    override val titles: Titles?,\n    override val canonicalTitle: String?,\n\n    override val number: Int?,\n    val seasonNumber: Int?,\n    val relativeNumber: Int?,\n    override val length: String?,\n    val airdate: String?,\n\n    override val thumbnail: Image?\n) : MediaUnit {\n\n    override val numberStringRes\n        get() = R.string.unit_episode\n\n    override val lengthStringRes: Int\n        get() = R.plurals.duration_minutes\n\n    override val date: String?\n        get() = airdate\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/media/unit/MediaUnit.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media.unit\n\nimport android.content.Context\nimport androidx.annotation.PluralsRes\nimport androidx.annotation.StringRes\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.util.DataUtil\nimport io.github.drumber.kitsune.util.formatDate\nimport io.github.drumber.kitsune.util.parseDate\nimport java.text.SimpleDateFormat\n\nsealed interface MediaUnit {\n    val id: String?\n\n    val description: String?\n    val titles: Titles?\n    val canonicalTitle: String?\n\n    val number: Int?\n    val length: String?\n    val thumbnail: Image?\n\n    //********************************************************************************************//\n\n    @get:StringRes\n    val numberStringRes: Int\n\n    @get:PluralsRes\n    val lengthStringRes: Int\n\n    val date: String?\n\n    fun hasValidTitle(): Boolean {\n        return DataUtil.getTitle(titles, canonicalTitle) != null &&\n                !Regex(\"(Chapter|Episode)\\\\s*\\\\d+\").matches(canonicalTitle ?: \"\")\n    }\n\n    fun title(context: Context) = if (hasValidTitle()) {\n        DataUtil.getTitle(titles, canonicalTitle)\n    } else {\n        numberText(context)\n    }\n\n    fun numberText(context: Context): String? {\n        return number?.let { context.getString(numberStringRes, it) }\n    }\n\n    fun formatDate(): String? {\n        return date?.parseDate()?.formatDate(SimpleDateFormat.SHORT)\n    }\n\n    fun length(context: Context): String? {\n        return length?.let {\n            context.resources.getQuantityString(lengthStringRes, it.toInt(), it)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/user/Favorite.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.user\n\ndata class Favorite(\n    val id: String,\n    val favRank: Int?,\n    val item: FavoriteItem?,\n    val user: User?\n)\n\ninterface FavoriteItem\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/user/User.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.user\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLink\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStats\n\ndata class User(\n    val id: String,\n    val createdAt: String?,\n\n    val name: String?,\n    val slug: String?,\n    val title: String?,\n\n    val avatar: Image?,\n    val coverImage: Image?,\n\n    val about: String?,\n    val location: String?,\n    val gender: String?,\n    val birthday: String?,\n    val waifuOrHusbando: String?,\n\n    val followersCount: Int?,\n    val followingCount: Int?,\n\n    val country: String?,\n    val language: String?,\n    val timeZone: String?,\n\n    val stats: List<UserStats>?,\n    val favorites: List<Favorite>?,\n    val waifu: Character?,\n    val profileLinks: List<ProfileLink>?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/user/profilelinks/ProfileLink.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.user.profilelinks\n\nimport io.github.drumber.kitsune.data.presentation.model.user.User\n\ndata class ProfileLink(\n    val id: String,\n    val url: String?,\n    val profileLinkSite: ProfileLinkSite?,\n    val user: User?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/user/profilelinks/ProfileLinkSite.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.user.profilelinks\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class ProfileLinkSite(\n    val id: String,\n    val name: String?\n) : Parcelable\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/user/stats/AmountConsumedPercentiles.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.user.stats\n\ndata class AmountConsumedPercentiles(\n    val media: Float?,\n    val units: Float?,\n    val time: Float?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/user/stats/UserStats.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.user.stats\n\ndata class UserStats(\n    val id: String,\n    val kind: UserStatsKind?,\n    val statsData: UserStatsData?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/user/stats/UserStatsData.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.user.stats\n\nsealed class UserStatsData {\n    data class CategoryBreakdownData(\n        val total: Int?,\n        val categories: Map<String, Int>?\n    ) : UserStatsData()\n\n    data class AmountConsumedData(\n        val time: Long?,\n        val media: Int?,\n        val units: Int?,\n        val completed: Int?,\n        val percentiles: AmountConsumedPercentiles?,\n        val averageDiffs: AmountConsumedPercentiles?\n    ) : UserStatsData()\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/presentation/model/user/stats/UserStatsKind.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.user.stats\n\nenum class UserStatsKind {\n    AnimeActivityHistory,\n    AnimeAmountConsumed,\n    AnimeCategoryBreakdown,\n    AnimeFavoriteYear,\n\n    MangaActivityHistory,\n    MangaAmountConsumed,\n    MangaCategoryBreakdown,\n    MangaFavoriteYear\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/AccessTokenRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.mapper.AuthMapper.toLocalAccessToken\nimport io.github.drumber.kitsune.data.source.local.auth.AccessTokenLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.AccessTokenNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.auth.model.ObtainAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.RefreshAccessToken\nimport io.github.drumber.kitsune.util.logD\nimport io.github.drumber.kitsune.util.logI\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport java.util.Date\n\nclass AccessTokenRepository(\n    private val localAccessTokenDataSource: AccessTokenLocalDataSource,\n    private val remoteAccessTokenDataSource: AccessTokenNetworkDataSource\n) {\n\n    private val mutex = Mutex()\n\n    private var isAccessTokenLoaded = false\n    private var cachedAccessToken: LocalAccessToken? = null\n\n    private val _accessTokenState by lazy {\n        MutableStateFlow(\n            if (hasAccessToken()) AccessTokenState.PRESENT\n            else AccessTokenState.NOT_PRESENT\n        )\n    }\n    val accessTokenState get() = _accessTokenState.asStateFlow()\n\n    fun getAccessToken(): LocalAccessToken? {\n        if (!isAccessTokenLoaded) {\n            cachedAccessToken = localAccessTokenDataSource.loadAccessToken()\n            isAccessTokenLoaded = true\n            cachedAccessToken?.let {\n                logI(\"Access token expires on: ${Date(it.getExpirationTimeInSeconds() * 1000)}\")\n            }\n        }\n        return cachedAccessToken\n    }\n\n    fun hasAccessToken(): Boolean {\n        return getAccessToken() != null\n    }\n\n    suspend fun clearAccessToken() {\n        mutex.withLock {\n            cachedAccessToken = null\n            localAccessTokenDataSource.clearAccessToken()\n            _accessTokenState.update { AccessTokenState.NOT_PRESENT }\n        }\n    }\n\n    suspend fun obtainAccessToken(username: String, password: String): LocalAccessToken {\n        mutex.withLock {\n            val accessToken = remoteAccessTokenDataSource.obtainAccessToken(\n                ObtainAccessToken(\n                    username = username,\n                    password = password\n                )\n            ).toLocalAccessToken()\n            storeAccessToken(accessToken)\n            _accessTokenState.update { AccessTokenState.PRESENT }\n            return accessToken\n        }\n    }\n\n    suspend fun refreshAccessToken(): LocalAccessToken {\n        val refreshToken = getAccessToken()?.refreshToken\n            ?: throw IllegalStateException(\"No refresh token available. Are you logged in?\")\n\n        mutex.withLock {\n            // Check if the access token was changed by a concurrent request\n            val localAccessToken = getAccessToken()\n            if (localAccessToken != null && localAccessToken.refreshToken != refreshToken) {\n                logD(\"Access token was updated by a concurrent request. Returning the updated token.\")\n                return localAccessToken\n            }\n\n            val accessToken = remoteAccessTokenDataSource.refreshToken(\n                RefreshAccessToken(\n                    refreshToken = refreshToken\n                )\n            ).toLocalAccessToken()\n            storeAccessToken(accessToken)\n            return accessToken\n        }\n    }\n\n    private fun storeAccessToken(accessToken: LocalAccessToken) {\n        localAccessTokenDataSource.storeAccessToken(accessToken)\n        cachedAccessToken = accessToken\n    }\n\n    enum class AccessTokenState {\n        NOT_PRESENT,\n        PRESENT\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/AlgoliaKeyRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.mapper.AlgoliaMapper.toAlgoliaKeyCollection\nimport io.github.drumber.kitsune.data.presentation.model.algolia.AlgoliaKeyCollection\nimport io.github.drumber.kitsune.data.source.network.algolia.AlgoliaKeyNetworkDataSource\n\nclass AlgoliaKeyRepository(\n    private val remoteAlgoliaKeyDataSource: AlgoliaKeyNetworkDataSource\n) {\n\n    private var cache: AlgoliaKeyCollection? = null\n\n    suspend fun getAllAlgoliaKeys(): AlgoliaKeyCollection {\n        return cache\n            ?: remoteAlgoliaKeyDataSource.getAllAlgoliaKeys().toAlgoliaKeyCollection().also {\n                cache = it\n            }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/AnimeRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.map\nimport io.github.drumber.kitsune.constants.Repository\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toAnime\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.source.network.media.AnimeNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.AnimePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.media.TrendingAnimePagingDataSource\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.flow.map\n\nclass AnimeRepository(\n    private val animeNetworkDataSource: AnimeNetworkDataSource\n) {\n\n    suspend fun getAllAnime(filter: Filter): List<Anime>? {\n        return animeNetworkDataSource.getAllAnime(filter).data?.map { it.toAnime() }\n    }\n\n    suspend fun getTrending(filter: Filter): List<Anime>? {\n        return animeNetworkDataSource.getTrending(filter).data?.map { it.toAnime() }\n    }\n\n    suspend fun getAnime(id: String, filter: Filter): Anime? {\n        return animeNetworkDataSource.getAnime(id, filter)?.toAnime()\n    }\n\n    suspend fun getLanguages(id: String): List<String> {\n        return animeNetworkDataSource.getLanguages(id)\n    }\n\n    fun animePager(filter: Filter, pageSize: Int) = Pager(\n        config = PagingConfig(\n            pageSize = pageSize,\n            maxSize = Repository.MAX_CACHED_ITEMS\n        ),\n        pagingSourceFactory = {\n            AnimePagingDataSource(animeNetworkDataSource, filter.pageLimit(pageSize))\n        }\n    ).flow.map { pagingData ->\n        pagingData.map { it.toAnime() }\n    }\n\n    fun trendingAnimePager(filter: Filter, pageSize: Int) = Pager(\n        config = PagingConfig(\n            pageSize = pageSize,\n            maxSize = Repository.MAX_CACHED_ITEMS\n        ),\n        pagingSourceFactory = {\n            TrendingAnimePagingDataSource(animeNetworkDataSource, filter.pageLimit(pageSize))\n        }\n    ).flow.map { pagingData ->\n        pagingData.map { it.toAnime() }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/AppUpdateRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.mapper.AppUpdateMapper.toAppRelease\nimport io.github.drumber.kitsune.data.presentation.model.appupdate.UpdateCheckResult\nimport io.github.drumber.kitsune.data.source.network.appupdate.AppReleaseNetworkDataSource\nimport io.github.drumber.kitsune.util.logE\n\nclass AppUpdateRepository(\n    private val appReleaseDataSource: AppReleaseNetworkDataSource\n) {\n\n    suspend fun checkForUpdates(currentVersion: String): UpdateCheckResult {\n        return try {\n            val release = appReleaseDataSource.getLatestRelease().toAppRelease()\n            if (isNewVersion(currentVersion, release.version)) {\n                UpdateCheckResult.NewVersion(release)\n            } else {\n                UpdateCheckResult.NoNewVersion\n            }\n        } catch (e: Exception) {\n            logE(\"Failed to fetch latest app release.\", e)\n            UpdateCheckResult.Error(e)\n        }\n    }\n\n    private fun isNewVersion(localVersion: String, remoteVersion: String): Boolean {\n        if (localVersion.trim() == remoteVersion.trim()) {\n            return false\n        }\n        return isSemanticVersionHigher(localVersion, remoteVersion)\n    }\n\n    private fun isSemanticVersionHigher(localVersion: String, remoteVersion: String): Boolean {\n        // replace everything that is not a digit at the beginning of the string\n        val regex = \"^\\\\D+\".toRegex()\n        // split the version strings into semantic version parts\n        val localParts = localVersion.replace(regex, \"\").trim().split(\".\")\n        val remoteParts = remoteVersion.replace(regex, \"\").trim().split(\".\")\n\n        for (i in 0 until maxOf(localParts.size, remoteParts.size)) {\n            val localPartString = localParts.getOrNull(i) ?: \"0\"\n            val remotePartString = remoteParts.getOrNull(i) ?: \"0\"\n\n            val localPartNumber = localPartString.toIntOrNull()\n            val remotePartNumber = remotePartString.toIntOrNull()\n\n            // Both parts are numbers: standard numeric comparison\n            if (remotePartNumber != null && localPartNumber != null) {\n                if (remotePartNumber != localPartNumber) {\n                    return remotePartNumber > localPartNumber\n                }\n                // both parts are equal, continue to next part\n                continue\n            }\n\n            // Remote is a number and local is a string starting with same number:\n            // => remote is stable version and has higher precedence (e.g. \"0\" vs \"0-beta1\")\n            if (remotePartNumber != null && localPartString.startsWith(remotePartString)) {\n                if (localPartString.endsWith(\"-debug\")) {\n                    // do not report debug versions as new versions\n                    return false\n                }\n                return true\n            }\n\n            // Local is a number and remote is a string starting with same number:\n            // => local is stable version and has higher precedence\n            if (localPartNumber != null && remotePartString.startsWith(localPartString)) {\n                return false\n            }\n\n            // Both parts are non-numeric: fallback to string comparison (e.g. \"0-beta1\" vs \"0-beta2\")\n            if (remotePartString != localPartString) {\n                return remotePartString > localPartString\n            }\n        }\n        return false\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/CastingRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.map\nimport io.github.drumber.kitsune.constants.Repository\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toCasting\nimport io.github.drumber.kitsune.data.source.network.media.CastingNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.CastingPagingDataSource\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.flow.map\n\nclass CastingRepository(\n    private val castingNetworkDataSource: CastingNetworkDataSource\n) {\n\n    fun castingPager(filter: Filter, pageSize: Int) = Pager(\n        config = PagingConfig(\n            pageSize = pageSize,\n            maxSize = Repository.MAX_CACHED_ITEMS\n        ),\n        pagingSourceFactory = {\n            CastingPagingDataSource(castingNetworkDataSource, filter.pageLimit(pageSize))\n        }\n    ).flow.map { pagingData ->\n        pagingData.map { it.toCasting() }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/CategoryRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toCategory\nimport io.github.drumber.kitsune.data.presentation.model.media.category.Category\nimport io.github.drumber.kitsune.data.source.network.media.CategoryNetworkDataSource\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass CategoryRepository(\n    private val categoryNetworkDataSource: CategoryNetworkDataSource\n) {\n\n    suspend fun getAllCategories(filter: Filter): List<Category>? {\n        return categoryNetworkDataSource.getAllCategories(filter)?.map { it.toCategory() }\n    }\n\n    suspend fun getCategory(id: String, filter: Filter): Category? {\n        return categoryNetworkDataSource.getCategory(id, filter)?.toCategory()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/CharacterRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toCharacter\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.source.network.character.CharacterNetworkDataSource\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass CharacterRepository(\n    private val characterNetworkDataSource: CharacterNetworkDataSource\n) {\n\n    suspend fun getCharacter(id: String, filter: Filter): Character? {\n        return characterNetworkDataSource.getCharacter(id, filter)?.toCharacter()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/FavoriteRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.mapper.UserMapper.toFavorite\nimport io.github.drumber.kitsune.data.presentation.model.user.Favorite\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.source.network.user.FavoriteNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavorite\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavoriteItem\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass FavoriteRepository(\n    private val remoteFavoriteDataSource: FavoriteNetworkDataSource\n) {\n\n    suspend fun getAllFavorites(filter: Filter): List<Favorite>? {\n        return remoteFavoriteDataSource.getAllFavorites(filter)?.map { it.toFavorite() }\n    }\n\n    suspend fun createMediaFavorite(userId: String, mediaType: MediaType, mediaId: String): Favorite? {\n        val favoriteItem = when (mediaType) {\n            MediaType.Anime -> NetworkAnime.empty(mediaId)\n            MediaType.Manga -> NetworkManga.empty(mediaId)\n        }\n        return createFavorite(userId, favoriteItem)\n    }\n\n    suspend fun createCharacterFavorite(userId: String, characterId: String): Favorite? {\n        val favoriteItem = NetworkCharacter(id = characterId)\n        return createFavorite(userId,favoriteItem)\n    }\n\n    private suspend fun createFavorite(userId: String, item: NetworkFavoriteItem): Favorite? {\n        val newFavorite = NetworkFavorite(\n            item = item,\n            user = NetworkUser(id = userId)\n        )\n        return remoteFavoriteDataSource.createFavorite(newFavorite)?.toFavorite()\n    }\n\n    suspend fun deleteFavorite(id: String): Boolean {\n        return remoteFavoriteDataSource.deleteFavorite(id)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/LibraryChangeListener.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport android.content.Context\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.domain.work.UpdateLibraryWidgetUseCase\n\ninterface LibraryChangeListener {\n    fun onNewLibraryEntry(libraryEntry: LibraryEntry)\n    fun onUpdateLibraryEntry(\n        libraryEntryModification: LibraryEntryModification,\n        updatedLibraryEntry: LibraryEntry?\n    )\n\n    fun onRemoveLibraryEntry(id: String)\n    fun onDataInsertion(libraryEntries: List<LibraryEntry>)\n}\n\nclass WidgetLibraryChangeListener(\n    private val context: Context,\n    private val updateLibraryWidget: UpdateLibraryWidgetUseCase\n) : LibraryChangeListener {\n\n    override fun onNewLibraryEntry(libraryEntry: LibraryEntry) {\n        if (libraryEntry.status == LibraryStatus.Current)\n            updateWidgets()\n    }\n\n    override fun onUpdateLibraryEntry(\n        libraryEntryModification: LibraryEntryModification,\n        updatedLibraryEntry: LibraryEntry?\n    ) {\n        updateWidgets()\n    }\n\n    override fun onRemoveLibraryEntry(id: String) {\n        updateWidgets()\n    }\n\n    override fun onDataInsertion(libraryEntries: List<LibraryEntry>) {\n        if (libraryEntries.any { it.status == LibraryStatus.Current })\n            updateWidgets()\n    }\n\n    private fun updateWidgets() {\n        updateLibraryWidget(context)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/LibraryEntryRemoteMediator.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport androidx.paging.ExperimentalPagingApi\nimport androidx.paging.LoadType\nimport androidx.paging.PagingState\nimport androidx.paging.RemoteMediator\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLocalLibraryEntry\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLocalLibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryFilter\nimport io.github.drumber.kitsune.data.source.local.LocalDatabase\nimport io.github.drumber.kitsune.data.source.local.library.LibraryLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntry\nimport io.github.drumber.kitsune.data.source.local.library.model.RemoteKeyEntity\nimport io.github.drumber.kitsune.data.source.local.library.model.RemoteKeyType\nimport io.github.drumber.kitsune.data.source.network.library.LibraryNetworkDataSource\nimport io.github.drumber.kitsune.util.logD\nimport io.github.drumber.kitsune.util.parseUtcDate\n\n@OptIn(ExperimentalPagingApi::class)\nclass LibraryEntryRemoteMediator(\n    private val filter: LibraryEntryFilter,\n    private val networkDataSource: LibraryNetworkDataSource,\n    private val localDataSource: LibraryLocalDataSource\n) : RemoteMediator<Int, LocalLibraryEntry>() {\n\n    /**\n     * Implementation based on android paging example from\n     * [Google Code Labs](https://github.com/googlecodelabs/android-paging/blob/78d231f6fbe9bf1326993362e1d08f823bef5ea2/app/src/main/java/com/example/android/codelabs/paging/data/GithubRemoteMediator.kt).\n     */\n    override suspend fun load(\n        loadType: LoadType,\n        state: PagingState<Int, LocalLibraryEntry>\n    ): MediatorResult {\n        return try {\n            val pageOffset = when (loadType) {\n                LoadType.REFRESH -> Kitsu.DEFAULT_PAGE_OFFSET\n                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)\n                LoadType.APPEND -> {\n                    val remoteKeys = getRemoteKeyForLastItem(state)\n                    // If remoteKeys is null, that means the refresh result is not in the database yet.\n                    // We can return Success with `endOfPaginationReached = false` because Paging\n                    // will call this method again if RemoteKeys becomes non-null.\n                    // If remoteKeys is NOT NULL but its prevKey is null, that means we've reached\n                    // the end of pagination for append.\n                    var nextKey = remoteKeys?.nextPageKey\n                        ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)\n\n                    state.pages.lastOrNull()?.prevKey?.let { prevPage ->\n                        // last page is equal or greater than reported next page: RemoteKey is out of sync\n                        if (prevPage >= nextKey) {\n                            localDataSource.deleteRemoteKeyByResourceId(remoteKeys.resourceId, remoteKeys.remoteKeyType)\n                            getRemoteKeyForLastItem(state)?.nextPageKey?.let {\n                                nextKey = it\n                            } ?: return MediatorResult.Success(endOfPaginationReached = false)\n                        }\n                    }\n                    nextKey\n                }\n            }\n\n            val pageData = networkDataSource.getAllLibraryEntries(\n                filter.buildFilter().pageOffset(pageOffset)\n            )\n            val data = pageData.data?.map { it.toLocalLibraryEntry() }\n                ?: throw NoDataException(\"Received data is 'null'.\")\n\n            localDataSource.runDatabaseTransaction {\n                // only clear database on REFRESH\n                if (loadType == LoadType.REFRESH) {\n                    logD(\"Clearing filtered library entries and corresponding remote keys from database.\")\n                    clearLibraryEntriesForFilterIgnoringNewerLibraryEntries(filter, data)\n                }\n\n                data.forEach { libraryEntry ->\n                    localDataSource.insertLibraryEntryIfUpdatedAtIsNewer(libraryEntry)\n                }\n\n                val remoteKeys = data.map {\n                    RemoteKeyEntity(it.id, RemoteKeyType.LibraryEntry, pageData.prev, pageData.next)\n                }\n                remoteKeyDao().insertALl(remoteKeys)\n            }\n\n            MediatorResult.Success(endOfPaginationReached = pageData.next == null)\n        } catch (e: Exception) {\n            MediatorResult.Error(e)\n        }\n    }\n\n    private suspend fun LocalDatabase.clearLibraryEntriesForFilterIgnoringNewerLibraryEntries(\n        filter: LibraryEntryFilter,\n        data: List<LocalLibraryEntry>\n    ) {\n        // clear all library entries in database with the selected kind and status\n        val libraryEntriesToBeCleared = localDataSource.getLibraryEntriesByKindAndStatus(\n            filter.kind,\n            filter.libraryStatus.map { it.toLocalLibraryStatus() }\n        ).filter { existingLibraryEntry ->\n            // do not clear library entries from database that are newer than the ones received\n            val updatedAtOfExistingEntry = existingLibraryEntry.updatedAt\n            val updatedAtOfNewEntry = data.find { it.id == existingLibraryEntry.id }?.updatedAt\n            if (updatedAtOfExistingEntry.isNullOrBlank() || updatedAtOfNewEntry.isNullOrBlank())\n                return@filter true\n\n            val existingEntryUpdateTime = updatedAtOfExistingEntry.parseUtcDate()?.time\n            val newEntryUpdateTime = updatedAtOfNewEntry.parseUtcDate()?.time\n            return@filter existingEntryUpdateTime == null || newEntryUpdateTime == null ||\n                    existingEntryUpdateTime <= newEntryUpdateTime\n        }\n\n        libraryEntryDao().deleteAll(libraryEntriesToBeCleared)\n        val remoteKeyIdsToClear = libraryEntriesToBeCleared.map { it.id }\n        remoteKeyDao().deleteAllByResourceId(remoteKeyIdsToClear, RemoteKeyType.LibraryEntry)\n    }\n\n    private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, LocalLibraryEntry>): RemoteKeyEntity? {\n        // Get the last page that was retrieved, that contained items.\n        // From that last page, get the last item that has a valid remote key.\n        return state.pages.lastOrNull { it.data.isNotEmpty() }?.data\n            ?.lastOrNull { libraryEntry ->\n                localDataSource.getRemoteKeyByResourceId(\n                    libraryEntry.id,\n                    RemoteKeyType.LibraryEntry\n                ) != null\n            }\n            ?.let { libraryEntry ->\n                // Get the remote keys of the last item retrieved\n                localDataSource.getRemoteKeyByResourceId(\n                    libraryEntry.id,\n                    RemoteKeyType.LibraryEntry\n                )\n            }\n    }\n\n    override suspend fun initialize(): InitializeAction {\n        return InitializeAction.LAUNCH_INITIAL_REFRESH\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/LibraryRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.map\nimport androidx.paging.ExperimentalPagingApi\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.map\nimport io.github.drumber.kitsune.constants.Repository\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.data.common.exception.NotFoundException\nimport io.github.drumber.kitsune.data.common.library.LibraryEntryKind\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLibraryEntry\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLibraryEntryModification\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLocalLibraryEntry\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLocalLibraryEntryModification\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLocalLibraryModificationState\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLocalLibraryStatus\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toNetworkLibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryFilter\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryModificationState\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.source.local.library.LibraryLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntry\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntryModification\nimport io.github.drumber.kitsune.data.source.network.library.LibraryEntryPagingDataSource\nimport io.github.drumber.kitsune.data.source.network.library.LibraryNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryEntry\nimport io.github.drumber.kitsune.data.utils.InvalidatingPagingSourceFactory\nimport io.github.drumber.kitsune.util.logD\nimport io.github.drumber.kitsune.util.parseUtcDate\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport retrofit2.HttpException\n\nclass LibraryRepository(\n    private val remoteLibraryDataSource: LibraryNetworkDataSource,\n    private val localLibraryDataSource: LibraryLocalDataSource,\n    private val libraryChangeListener: LibraryChangeListener,\n    private val coroutineScope: CoroutineScope\n) {\n\n    private val filterForFullLibraryEntry\n        get() = Filter().include(\"anime\", \"manga\")\n\n    suspend fun addNewLibraryEntry(\n        userId: String,\n        media: Media,\n        status: LibraryStatus\n    ): LibraryEntry? {\n        val newLibraryEntry = NetworkLibraryEntry.new(\n            userId,\n            media.mediaType,\n            media.id,\n            status.toNetworkLibraryStatus()\n        )\n\n        return coroutineScope.async {\n            val libraryEntry = remoteLibraryDataSource.postLibraryEntry(\n                newLibraryEntry,\n                filterForFullLibraryEntry\n            )\n\n            if (libraryEntry != null) {\n                localLibraryDataSource.insertLibraryEntry(libraryEntry.toLocalLibraryEntry())\n            }\n            libraryEntry?.toLibraryEntry()\n        }.await().also { libraryEntry ->\n            libraryEntry?.let { libraryChangeListener.onNewLibraryEntry(it) }\n        }\n    }\n\n    suspend fun removeLibraryEntry(libraryEntryId: String) {\n        remoteLibraryDataSource.deleteLibraryEntry(libraryEntryId)\n        localLibraryDataSource.deleteLibraryEntryAndAnyModification(libraryEntryId)\n        libraryChangeListener.onRemoveLibraryEntry(libraryEntryId)\n    }\n\n    /**\n     * Check if library entry was deleted on the server. If so, remove it from local database.\n     */\n    suspend fun mayRemoveLibraryEntryLocally(libraryEntryId: String) {\n        if (!doesLibraryEntryExist(libraryEntryId)) {\n            localLibraryDataSource.deleteLibraryEntryAndAnyModification(libraryEntryId)\n            libraryChangeListener.onRemoveLibraryEntry(libraryEntryId)\n        }\n    }\n\n    private suspend fun doesLibraryEntryExist(libraryEntryId: String): Boolean {\n        return try {\n            remoteLibraryDataSource.getLibraryEntry(\n                libraryEntryId,\n                Filter().fields(\"libraryEntries\", \"id\")\n            ) != null\n        } catch (e: HttpException) {\n            if (e.code() == 404)\n                return false\n            throw e\n        }\n    }\n\n    suspend fun updateLibraryEntry(\n        libraryEntryModification: LibraryEntryModification\n    ): LibraryEntry {\n        val modification =\n            libraryEntryModification.copy(state = LibraryModificationState.SYNCHRONIZING)\n        var libraryEntry: LibraryEntry? = null\n\n        return try {\n            coroutineScope.async {\n                localLibraryDataSource.insertLibraryEntryModification(modification.toLocalLibraryEntryModification())\n            }.await()\n\n            val networkLibraryEntry = pushModificationToService(modification)\n            coroutineScope.async {\n                if (isLibraryEntryNotOlderThanInDatabase(networkLibraryEntry.toLocalLibraryEntry())) {\n                    localLibraryDataSource.updateLibraryEntryAndDeleteModification(\n                        networkLibraryEntry.toLocalLibraryEntry(),\n                        modification.toLocalLibraryEntryModification()\n                    )\n                }\n            }.await()\n            networkLibraryEntry.toLibraryEntry().also { libraryEntry = it }\n        } catch (e: NotFoundException) {\n            localLibraryDataSource.deleteLibraryEntryAndAnyModification(modification.id)\n            throw e\n        } catch (e: Exception) {\n            insertLocalModificationOrDeleteIfSameAsLibraryEntry(\n                modification.copy(state = LibraryModificationState.NOT_SYNCHRONIZED)\n                    .toLocalLibraryEntryModification()\n            )\n            throw e\n        } finally {\n            libraryChangeListener.onUpdateLibraryEntry(libraryEntryModification, libraryEntry)\n        }\n    }\n\n    suspend fun fetchAndStoreLibraryEntryForMedia(userId: String, media: Media): LibraryEntry? {\n        val requestFilter = filterForFullLibraryEntry.copy()\n            .filter(\"user_id\", userId)\n        when (media) {\n            is Anime -> requestFilter.filter(\"anime_id\", media.id)\n            is Manga -> requestFilter.filter(\"manga_id\", media.id)\n        }\n\n        val filter = LibraryEntryFilter(\n            kind = LibraryEntryKind.All,\n            libraryStatus = emptyList(),\n            initialFilter = requestFilter\n        )\n        return fetchAndStoreLibraryEntriesForFilter(filter)?.firstOrNull()\n    }\n\n    suspend fun fetchAndStoreLibraryEntriesForFilter(filter: LibraryEntryFilter): List<LibraryEntry>? {\n        val libraryEntries = remoteLibraryDataSource.getAllLibraryEntries(filter.buildFilter()).data\n        if (libraryEntries != null) {\n            localLibraryDataSource.insertAllLibraryEntries(libraryEntries.map { it.toLocalLibraryEntry() })\n        }\n        return libraryEntries?.map { it.toLibraryEntry() }?.also {\n            libraryChangeListener.onDataInsertion(it)\n        }\n    }\n\n    suspend fun fetchAllLibraryEntries(filter: Filter): List<LibraryEntry>? {\n        return remoteLibraryDataSource.getAllLibraryEntries(filter).data?.map { it.toLibraryEntry() }\n    }\n\n    suspend fun fetchLibraryEntry(id: String, filter: Filter): LibraryEntry? {\n        return remoteLibraryDataSource.getLibraryEntry(id, filter)?.toLibraryEntry()\n    }\n\n    suspend fun getLibraryEntryFromDatabase(id: String): LibraryEntry? {\n        return localLibraryDataSource.getLibraryEntry(id)?.toLibraryEntry()\n    }\n\n    suspend fun getLibraryEntryFromMedia(mediaId: String): LibraryEntry? {\n        return localLibraryDataSource.getLibraryEntryFromMedia(mediaId)?.toLibraryEntry()\n    }\n\n    suspend fun getLibraryEntriesWithModificationsByStatus(status: List<LibraryStatus>): List<LibraryEntryWithModification> {\n        return localLibraryDataSource.getLibraryEntriesWithModificationsByStatus(\n            status.map { it.toLocalLibraryStatus() }\n        ).map {\n            LibraryEntryWithModification(\n                libraryEntry = it.libraryEntry.toLibraryEntry(),\n                modification = it.libraryEntryModification?.toLibraryEntryModification()\n            )\n        }\n    }\n\n    fun getLibraryEntriesWithModificationsByStatusAsFlow(status: List<LibraryStatus>): Flow<List<LibraryEntryWithModification>> {\n        return localLibraryDataSource.getLibraryEntriesWithModificationsByStatusAsFlow(\n            status.map { it.toLocalLibraryStatus() }\n        ).map { entries ->\n            entries.map {\n                LibraryEntryWithModification(\n                    libraryEntry = it.libraryEntry.toLibraryEntry(),\n                    modification = it.libraryEntryModification?.toLibraryEntryModification()\n                )\n            }\n        }\n    }\n\n    fun getLibraryEntryWithModificationFromMediaAsLiveData(mediaId: String): LiveData<LibraryEntryWithModification?> {\n        return localLibraryDataSource.getLibraryEntryWithModificationFromMediaAsLiveData(mediaId)\n            .map { entry ->\n                entry?.let {\n                    LibraryEntryWithModification(\n                        libraryEntry = it.libraryEntry.toLibraryEntry(),\n                        modification = it.libraryEntryModification?.toLibraryEntryModification()\n                    )\n                }\n            }\n    }\n\n    //********************************************************************************************//\n    // Library modifications related methods\n    //********************************************************************************************//\n\n    suspend fun getLibraryEntryModification(id: String): LibraryEntryModification? {\n        return localLibraryDataSource.getLibraryEntryModification(id)?.toLibraryEntryModification()\n    }\n\n    suspend fun getAllLibraryEntryModifications(): List<LibraryEntryModification> {\n        return localLibraryDataSource.getAllLocalLibraryModifications()\n            .map { it.toLibraryEntryModification() }\n    }\n\n    fun getLibraryEntryModificationsAsFlow(): Flow<List<LibraryEntryModification>> {\n        return localLibraryDataSource.getAllLibraryEntryModificationsAsFlow()\n            .map { modifications ->\n                modifications.map { it.toLibraryEntryModification() }\n            }\n    }\n\n    fun getLibraryEntryModificationsByStateAsLiveData(state: LibraryModificationState): LiveData<List<LibraryEntryModification>> {\n        return localLibraryDataSource\n            .getLibraryEntryModificationsByStateAsLiveData(state.toLocalLibraryModificationState())\n            .map { modifications ->\n                modifications.map { it.toLibraryEntryModification() }\n            }\n    }\n\n    private suspend fun pushModificationToService(\n        modification: LibraryEntryModification\n    ): NetworkLibraryEntry {\n        val updatedLibraryEntry = NetworkLibraryEntry.update(\n            id = modification.id,\n            startedAt = modification.startedAt,\n            finishedAt = modification.finishedAt,\n            status = modification.status?.toNetworkLibraryStatus(),\n            progress = modification.progress,\n            reconsumeCount = modification.reconsumeCount,\n            volumesOwned = modification.volumesOwned,\n            ratingTwenty = modification.ratingTwenty,\n            notes = modification.notes,\n            isPrivate = modification.privateEntry,\n        )\n\n        try {\n            val libraryEntry = remoteLibraryDataSource.updateLibraryEntry(\n                modification.id,\n                updatedLibraryEntry,\n                filterForFullLibraryEntry\n            )\n                ?: throw NoDataException(\"Received library entry for ID '${modification.id}' is 'null'.\")\n            return libraryEntry\n        } catch (e: HttpException) {\n            if (e.code() == 404) {\n                throw NotFoundException(\n                    \"Library entry with ID '${modification.id}' does not exist.\",\n                    e\n                )\n            }\n            throw e\n        }\n    }\n\n    private suspend fun insertLocalModificationOrDeleteIfSameAsLibraryEntry(\n        libraryEntryModification: LocalLibraryEntryModification\n    ) {\n        val modificationInDb =\n            localLibraryDataSource.getLibraryEntryModification(libraryEntryModification.id)\n        if (modificationInDb != null && modificationInDb.createTime > libraryEntryModification.createTime) {\n            logD(\"Modification in database is newer than the one being inserted. Ignoring $libraryEntryModification\")\n            return\n        }\n\n        val libraryEntry = localLibraryDataSource.getLibraryEntry(libraryEntryModification.id)\n        if (libraryEntry != null && libraryEntryModification.isEqualToLibraryEntry(libraryEntry)) {\n            localLibraryDataSource.deleteLibraryEntryModification(libraryEntryModification)\n        } else {\n            localLibraryDataSource.insertLibraryEntryModification(libraryEntryModification)\n        }\n    }\n\n    private suspend fun isLibraryEntryNotOlderThanInDatabase(libraryEntry: LocalLibraryEntry): Boolean {\n        return localLibraryDataSource.getLibraryEntry(libraryEntry.id)?.let { dbEntry ->\n            val dbUpdatedAt = dbEntry.updatedAt?.parseUtcDate() ?: return true\n            val thisUpdatedAt = libraryEntry.updatedAt?.parseUtcDate() ?: return true\n            thisUpdatedAt.time >= dbUpdatedAt.time\n        } ?: true\n    }\n\n    //********************************************************************************************//\n    // Paging related methods\n    //********************************************************************************************//\n\n    private val invalidatingPagingSourceFactory =\n        InvalidatingPagingSourceFactory<Int, LocalLibraryEntry, LibraryEntryFilter> { filter ->\n            localLibraryDataSource.getLibraryEntriesByKindAndStatusAsPagingSource(\n                kind = filter.kind,\n                status = filter.libraryStatus.map { it.toLocalLibraryStatus() }\n            )\n        }\n\n    @OptIn(ExperimentalPagingApi::class)\n    fun libraryEntriesPager(pageSize: Int, filter: LibraryEntryFilter) = Pager(\n        config = PagingConfig(\n            pageSize = pageSize,\n            maxSize = Repository.MAX_CACHED_ITEMS\n        ),\n        remoteMediator = LibraryEntryRemoteMediator(\n            filter.pageSize(pageSize),\n            remoteLibraryDataSource,\n            localLibraryDataSource\n        ),\n        pagingSourceFactory = { invalidatingPagingSourceFactory.createPagingSource(filter) }\n    ).flow.map { pagingData ->\n        pagingData.map { it.toLibraryEntry() }\n    }\n\n    fun searchLibraryEntriesPager(pageSize: Int, filter: Filter) = Pager(\n        config = PagingConfig(\n            pageSize = pageSize,\n            maxSize = Repository.MAX_CACHED_ITEMS\n        ),\n        pagingSourceFactory = {\n            LibraryEntryPagingDataSource(\n                remoteLibraryDataSource,\n                filter.pageLimit(pageSize)\n            )\n        }\n    ).flow.map { pagingData ->\n        pagingData.map { it.toLibraryEntry() }\n    }\n\n    fun invalidatePagingSources() {\n        invalidatingPagingSourceFactory.invalidate()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/MangaRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.map\nimport io.github.drumber.kitsune.constants.Repository\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toManga\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.source.network.media.MangaNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.MangaPagingDataSource\nimport io.github.drumber.kitsune.data.source.network.media.TrendingMangaPagingDataSource\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.flow.map\n\nclass MangaRepository(\n    private val mangaNetworkDataSource: MangaNetworkDataSource\n) {\n\n    suspend fun getAllManga(filter: Filter): List<Manga>? {\n        return mangaNetworkDataSource.getAllManga(filter).data?.map { it.toManga() }\n    }\n\n    suspend fun getTrending(filter: Filter): List<Manga>? {\n        return mangaNetworkDataSource.getTrending(filter).data?.map { it.toManga() }\n    }\n\n    suspend fun getManga(id: String, filter: Filter): Manga? {\n        return mangaNetworkDataSource.getManga(id, filter)?.toManga()\n    }\n\n    fun mangaPager(filter: Filter, pageSize: Int) = Pager(\n        config = PagingConfig(\n            pageSize = pageSize,\n            maxSize = Repository.MAX_CACHED_ITEMS\n        ),\n        pagingSourceFactory = {\n            MangaPagingDataSource(mangaNetworkDataSource, filter.pageLimit(pageSize))\n        }\n    ).flow.map { pagingData ->\n        pagingData.map { it.toManga() }\n    }\n\n    fun trendingMangaPager(filter: Filter, pageSize: Int) = Pager(\n        config = PagingConfig(\n            pageSize = pageSize,\n            maxSize = Repository.MAX_CACHED_ITEMS\n        ),\n        pagingSourceFactory = {\n            TrendingMangaPagingDataSource(mangaNetworkDataSource, filter.pageLimit(pageSize))\n        }\n    ).flow.map { pagingData ->\n        pagingData.map { it.toManga() }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/MappingRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.mapper.MappingMapper.toMapping\nimport io.github.drumber.kitsune.data.presentation.model.mapping.Mapping\nimport io.github.drumber.kitsune.data.source.network.mapping.MappingNetworkDataSource\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass MappingRepository(\n    private val mappingNetworkDataSource: MappingNetworkDataSource\n) {\n\n    suspend fun getAnimeMappings(animeId: String, filter: Filter = Filter()): List<Mapping>? {\n        return mappingNetworkDataSource.getAnimeMappings(animeId, filter)?.map { it.toMapping() }\n    }\n\n    suspend fun getMangaMappings(mangaId: String, filter: Filter = Filter()): List<Mapping>? {\n        return mappingNetworkDataSource.getMangaMappings(mangaId, filter)?.map { it.toMapping() }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/MediaUnitRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.map\nimport io.github.drumber.kitsune.constants.Repository\nimport io.github.drumber.kitsune.data.mapper.MediaUnitMapper.toMediaUnit\nimport io.github.drumber.kitsune.data.source.network.media.ChapterNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.ChapterPagingDataSource\nimport io.github.drumber.kitsune.data.source.network.media.EpisodeNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.EpisodePagingDataSource\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.flow.map\n\nclass MediaUnitRepository(\n    private val episodeNetworkDataSource: EpisodeNetworkDataSource,\n    private val chapterNetworkDataSource: ChapterNetworkDataSource\n) {\n\n    fun mediaUnitPager(type: MediaUnitType, filter: Filter, pageSize: Int) = Pager(\n        config = PagingConfig(\n            pageSize = pageSize,\n            maxSize = Repository.MAX_CACHED_ITEMS\n        ),\n        pagingSourceFactory = {\n            when (type) {\n                MediaUnitType.EPISODE -> EpisodePagingDataSource(episodeNetworkDataSource, filter.pageLimit(pageSize))\n                MediaUnitType.CHAPTER -> ChapterPagingDataSource(chapterNetworkDataSource, filter.pageLimit(pageSize))\n            }\n        }\n    ).flow.map { pagingData ->\n        pagingData.map { it.toMediaUnit() }\n    }\n\n    enum class MediaUnitType {\n        EPISODE,\n        CHAPTER\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/ProfileLinkRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.mapper.ProfileLinksMapper.toProfileLink\nimport io.github.drumber.kitsune.data.mapper.ProfileLinksMapper.toProfileLinkSite\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLink\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLinkSite\nimport io.github.drumber.kitsune.data.source.network.user.ProfileLinkNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLinkSite\n\nclass ProfileLinkRepository(\n    private val remoteProfileLinkDataSource: ProfileLinkNetworkDataSource\n) {\n\n    suspend fun getAllProfileLinkSites(filter: Filter): List<ProfileLinkSite>? {\n        return remoteProfileLinkDataSource.getAllProfileLinkSites(filter)\n            ?.map { it.toProfileLinkSite() }\n    }\n\n    suspend fun createProfileLink(userId: String, siteId: String, url: String): ProfileLink? {\n        val newProfileLink = NetworkProfileLink.empty().copy(\n            url = url,\n            profileLinkSite = NetworkProfileLinkSite(\n                id = siteId,\n                name = null\n            ),\n            user = NetworkUser(id = userId)\n        )\n        return remoteProfileLinkDataSource.createProfileLink(newProfileLink)?.toProfileLink()\n    }\n\n    suspend fun updateProfileLink(\n        userId: String,\n        profileLinkId: String,\n        url: String\n    ): ProfileLink? {\n        val updatedProfileLink = NetworkProfileLink.empty().copy(\n            id = profileLinkId,\n            url = url,\n            user = NetworkUser(id = userId)\n        )\n        return remoteProfileLinkDataSource.updateProfileLink(profileLinkId, updatedProfileLink)\n            ?.toProfileLink()\n    }\n\n    suspend fun deleteProfileLink(id: String): Boolean {\n        return remoteProfileLinkDataSource.deleteProfileLink(id)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/repository/UserRepository.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.constants.Defaults\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.data.mapper.ProfileLinksMapper.toProfileLink\nimport io.github.drumber.kitsune.data.mapper.UserMapper.toLocalUser\nimport io.github.drumber.kitsune.data.mapper.UserMapper.toNetworkUser\nimport io.github.drumber.kitsune.data.mapper.UserMapper.toUser\nimport io.github.drumber.kitsune.data.presentation.model.user.User\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLink\nimport io.github.drumber.kitsune.data.source.local.user.UserLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.data.source.network.user.UserNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUserImageUpload\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\n\nclass UserRepository(\n    private val localUserDataSource: UserLocalDataSource,\n    private val remoteUserDataSource: UserNetworkDataSource,\n    private val coroutineScope: CoroutineScope\n) {\n\n    private val storeLocalUserMutex = Mutex()\n\n    private val _localUser = MutableStateFlow(localUserDataSource.loadUser())\n    val localUser = _localUser.asStateFlow()\n\n    private val _userReLogInPrompt = MutableSharedFlow<Unit>()\n    val userReLogInPrompt = _userReLogInPrompt.asSharedFlow()\n\n    fun hasLocalUser() = _localUser.value != null\n\n    fun clearLocalUser() {\n        localUserDataSource.clearUser()\n        _localUser.value = null\n    }\n\n    /**\n     * Fetches the app user from the network and updates the local cached user object.\n     */\n    suspend fun fetchAndStoreLocalUserFromNetwork() {\n        val baseFilter = Filter()\n            .include(\"waifu\")\n            .fields(\"characters\", *Defaults.MINIMUM_CHARACTER_FIELDS)\n\n        // fetch user model in the repository scope\n        coroutineScope.async {\n            val user = remoteUserDataSource.getSelf(baseFilter)?.toLocalUser()\n                ?: throw NoDataException()\n            storeLocalUser(user)\n        }.await()\n    }\n\n    suspend fun storeLocalUser(user: LocalUser) {\n        storeLocalUserMutex.withLock {\n            localUserDataSource.storeUser(user)\n            _localUser.value = user\n        }\n    }\n\n    suspend fun promptUserReLogIn() {\n        _userReLogInPrompt.emit(Unit)\n    }\n\n    suspend fun fetchUser(userId: String, filter: Filter): User? {\n        return remoteUserDataSource.getUser(userId, filter)?.toUser()\n    }\n\n    suspend fun updateUser(userId: String, user: LocalUser): LocalUser? {\n        return remoteUserDataSource.updateUser(userId, user.toNetworkUser())?.toLocalUser()\n    }\n\n    suspend fun updateUserImage(userId: String, avatar: String?, coverImage: String?): Boolean {\n        val userImageUpload = NetworkUserImageUpload(\n            id = userId,\n            avatar = avatar,\n            coverImage = coverImage\n        )\n        return remoteUserDataSource.updateUserImage(userId, userImageUpload)\n    }\n\n    suspend fun deleteWaifuRelationship(userId: String): Boolean {\n        return remoteUserDataSource.deleteWaifuRelationship(userId)\n    }\n\n    suspend fun getProfileLinksForUser(userId: String, filter: Filter): List<ProfileLink>? {\n        return remoteUserDataSource.getProfileLinksForUser(userId, filter)\n            ?.map { it.toProfileLink() }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/LocalDatabase.kt",
    "content": "package io.github.drumber.kitsune.data.source.local\n\nimport android.app.Application\nimport androidx.room.Database\nimport androidx.room.Room\nimport androidx.room.RoomDatabase\nimport androidx.room.TypeConverters\nimport io.github.drumber.kitsune.data.source.local.library.LocalLibraryConverters\nimport io.github.drumber.kitsune.data.source.local.library.dao.LibraryEntryDao\nimport io.github.drumber.kitsune.data.source.local.library.dao.LibraryEntryModificationDao\nimport io.github.drumber.kitsune.data.source.local.library.dao.LibraryEntryWithModificationDao\nimport io.github.drumber.kitsune.data.source.local.library.dao.RemoteKeyDao\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntry\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntryModification\nimport io.github.drumber.kitsune.data.source.local.library.model.RemoteKeyEntity\n\n@Database(\n    entities = [\n        LocalLibraryEntry::class, LocalLibraryEntryModification::class, RemoteKeyEntity::class\n    ],\n    version = 3,\n    exportSchema = true\n)\n@TypeConverters(LocalLibraryConverters::class)\nabstract class LocalDatabase : RoomDatabase() {\n\n    abstract fun libraryEntryDao(): LibraryEntryDao\n    abstract fun libraryEntryModificationDao(): LibraryEntryModificationDao\n    abstract fun libraryEntryWithModificationDao(): LibraryEntryWithModificationDao\n    abstract fun remoteKeyDao(): RemoteKeyDao\n\n    companion object {\n        fun createLocalDatabase(application: Application): LocalDatabase {\n            return Room.databaseBuilder(application, LocalDatabase::class.java, \"kitsune.db\")\n                .fallbackToDestructiveMigration()\n                .build()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/auth/AccessTokenLocalDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.auth\n\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\n\ninterface AccessTokenLocalDataSource {\n\n    fun loadAccessToken(): LocalAccessToken?\n\n    fun storeAccessToken(accessToken: LocalAccessToken)\n\n    fun clearAccessToken()\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/auth/AccessTokenPreference.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.auth\n\nimport android.content.Context\nimport androidx.core.content.edit\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.util.logI\n\nclass AccessTokenPreference(\n    context: Context,\n    private val objectMapper: ObjectMapper\n): AccessTokenLocalDataSource {\n\n    private val masterKey = MasterKey.Builder(context)\n        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n        .build()\n\n    private val sharedPrefsFile = context.getString(R.string.auth_preference_file_key)\n    private val sharedPreferences = EncryptedSharedPreferences.create(\n        context,\n        sharedPrefsFile,\n        masterKey,\n        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n    )\n\n    override fun storeAccessToken(accessToken: LocalAccessToken) {\n        logI(\"Converting access token to json and storing it in encrypted shared preferences.\")\n        val jsonString = objectMapper.writeValueAsString(accessToken)\n        sharedPreferences.edit(commit = true) {\n            putString(KEY_ACCESS_TOKEN, jsonString)\n        }\n    }\n\n    override fun clearAccessToken() {\n        logI(\"Deleting access token from encrypted shared preferences.\")\n        sharedPreferences.edit(commit = true) {\n            remove(KEY_ACCESS_TOKEN)\n        }\n    }\n\n    override fun loadAccessToken(): LocalAccessToken? {\n        val jsonString = sharedPreferences.getString(KEY_ACCESS_TOKEN, null)\n        return if (!jsonString.isNullOrBlank()) {\n            logI(\"Parse and return access token stored as json.\")\n            objectMapper.readValue(jsonString)\n        } else {\n            logI(\"No access token stored.\")\n            null\n        }\n    }\n\n    companion object {\n        const val KEY_ACCESS_TOKEN = \"access_token\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/auth/model/LocalAccessToken.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.auth.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\ndata class LocalAccessToken(\n    @JsonProperty(\"access_token\")\n    val accessToken: String,\n    @JsonProperty(\"created_at\")\n    val createdAt: Long,\n    @JsonProperty(\"expires_in\")\n    val expiresIn: Long,\n    @JsonProperty(\"refresh_token\")\n    val refreshToken: String\n) {\n\n    fun getExpirationTimeInSeconds(): Long {\n        return createdAt + expiresIn\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/character/LocalCharacter.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.character\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\n\ndata class LocalCharacter(\n    val id: String,\n    val slug: String?,\n    val name: String?,\n    val names: Titles?,\n    val otherNames: List<String>?,\n    val malId: Int?,\n    val description: String?,\n    val image: Image?\n) {\n    companion object {\n        fun empty(id: String) = LocalCharacter(\n            id = id,\n            slug = null,\n            name = null,\n            names = null,\n            otherNames = null,\n            malId = null,\n            description = null,\n            image = null\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/LibraryLocalDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library\n\nimport androidx.paging.PagingSource\nimport androidx.room.withTransaction\nimport io.github.drumber.kitsune.data.common.library.LibraryEntryKind\nimport io.github.drumber.kitsune.data.source.local.LocalDatabase\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntry\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntryModification\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryMedia.MediaType\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryModificationState\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryStatus\nimport io.github.drumber.kitsune.data.source.local.library.model.RemoteKeyType\n\nclass LibraryLocalDataSource(\n    private val database: LocalDatabase\n) {\n\n    private val libraryEntryDao\n        get() = database.libraryEntryDao()\n    private val libraryEntryModificationDao\n        get() = database.libraryEntryModificationDao()\n    private val libraryEntryWithModificationDao\n        get() = database.libraryEntryWithModificationDao()\n    private val remoteKeyDao\n        get() = database.remoteKeyDao()\n\n    //********************************************************************************************//\n    // LibraryEntry related methods\n    //********************************************************************************************//\n\n    suspend fun getLibraryEntry(id: String) = libraryEntryDao.getLibraryEntry(id)\n\n    suspend fun getLibraryEntryFromMedia(mediaId: String) =\n        libraryEntryDao.getLibraryEntryFromMedia(mediaId)\n\n    suspend fun getLibraryEntriesWithModificationsByStatus(status: List<LocalLibraryStatus>) =\n        libraryEntryWithModificationDao.getLibraryEntriesWithModificationByStatus(status)\n\n    fun getLibraryEntriesWithModificationsByStatusAsFlow(status: List<LocalLibraryStatus>) =\n        libraryEntryWithModificationDao.getLibraryEntriesWithModificationByStatusAsFlow(status)\n\n    fun getLibraryEntryWithModificationFromMediaAsLiveData(mediaId: String) =\n        libraryEntryWithModificationDao.getLibraryEntryWithModificationFromMediaAsLiveData(mediaId)\n\n    suspend fun insertAllLibraryEntries(libraryEntries: List<LocalLibraryEntry>) {\n        database.withTransaction {\n            libraryEntries.forEach {\n                insertLibraryEntry(it)\n            }\n        }\n    }\n\n    suspend fun insertLibraryEntry(libraryEntry: LocalLibraryEntry) {\n        libraryEntry.verifyIsValidLibraryEntry()\n        insertLibraryEntryIfUpdatedAtIsNewer(libraryEntry)\n    }\n\n    suspend fun insertLibraryEntryIfUpdatedAtIsNewer(libraryEntry: LocalLibraryEntry): Boolean {\n        libraryEntry.verifyIsValidLibraryEntry()\n        if (libraryEntry.updatedAt.isNullOrBlank()) {\n            insertLibraryEntry(libraryEntry)\n            return true\n        }\n        return database.withTransaction {\n            val hasNewerEntry = libraryEntryDao.hasLibraryEntryWhereUpdatedAtIsAfter(\n                libraryEntry.id,\n                libraryEntry.updatedAt\n            )\n            if (!hasNewerEntry) {\n                libraryEntryDao.insertSingle(libraryEntry)\n                true\n            } else {\n                // do not overwrite more up-to-date library entry\n                false\n            }\n        }\n    }\n\n    suspend fun getLibraryEntriesByKindAndStatus(\n        kind: LibraryEntryKind,\n        status: List<LocalLibraryStatus>\n    ): List<LocalLibraryEntry> {\n        val hasStatus = status.isNotEmpty()\n        return with(libraryEntryDao) {\n            when {\n                kind == LibraryEntryKind.Anime && hasStatus -> getAllLibraryEntriesByTypeAndStatus(\n                    MediaType.Anime,\n                    status\n                )\n\n                kind == LibraryEntryKind.Anime && !hasStatus -> getAllLibraryEntriesByType(\n                    MediaType.Anime\n                )\n\n                kind == LibraryEntryKind.Manga && hasStatus -> getAllLibraryEntriesByTypeAndStatus(\n                    MediaType.Manga,\n                    status\n                )\n\n                kind == LibraryEntryKind.Manga && !hasStatus -> getAllLibraryEntriesByType(\n                    MediaType.Manga\n                )\n\n                kind == LibraryEntryKind.All && hasStatus -> getAllLibraryEntriesByStatus(\n                    status\n                )\n\n                else -> getAllLibraryEntries()\n            }\n        }\n    }\n\n    fun getLibraryEntriesByKindAndStatusAsPagingSource(\n        kind: LibraryEntryKind,\n        status: List<LocalLibraryStatus>\n    ): PagingSource<Int, LocalLibraryEntry> {\n        val hasStatus = status.isNotEmpty()\n        return with(libraryEntryDao) {\n            when {\n                kind == LibraryEntryKind.Anime && hasStatus -> allLibraryEntriesByTypeAndStatusPagingSource(\n                    MediaType.Anime,\n                    status\n                )\n\n                kind == LibraryEntryKind.Anime && !hasStatus -> allLibraryEntriesByTypePagingSource(\n                    MediaType.Anime\n                )\n\n                kind == LibraryEntryKind.Manga && hasStatus -> allLibraryEntriesByTypeAndStatusPagingSource(\n                    MediaType.Manga,\n                    status\n                )\n\n                kind == LibraryEntryKind.Manga && !hasStatus -> allLibraryEntriesByTypePagingSource(\n                    MediaType.Manga\n                )\n\n                kind == LibraryEntryKind.All && hasStatus -> allLibraryEntriesByStatusPagingSource(\n                    status\n                )\n\n                else -> allLibraryEntriesPagingSource()\n            }\n        }\n    }\n\n    //********************************************************************************************//\n    // LibraryEntryModification related methods\n    //********************************************************************************************//\n\n    suspend fun getLibraryEntryModification(id: String) =\n        libraryEntryModificationDao.getLibraryEntryModification(id)\n\n    suspend fun getAllLocalLibraryModifications(): List<LocalLibraryEntryModification> {\n        return libraryEntryModificationDao.getAllLibraryEntryModifications()\n    }\n\n    fun getAllLibraryEntryModificationsAsFlow() =\n        libraryEntryModificationDao.getAllLibraryEntryModificationsAsFlow()\n\n    fun getLibraryEntryModificationsByStateAsLiveData(state: LocalLibraryModificationState) =\n        libraryEntryModificationDao.getLibraryEntryModificationsByStateAsLiveData(state)\n\n    suspend fun insertLibraryEntryModification(\n        libraryEntryModification: LocalLibraryEntryModification\n    ) {\n        libraryEntryModificationDao.insertSingle(libraryEntryModification)\n    }\n\n    suspend fun deleteLibraryEntryModification(\n        libraryEntryModification: LocalLibraryEntryModification\n    ) {\n        libraryEntryModificationDao.deleteSingle(libraryEntryModification)\n    }\n\n    suspend fun updateLibraryEntryAndDeleteModification(\n        libraryEntry: LocalLibraryEntry,\n        libraryEntryModification: LocalLibraryEntryModification\n    ) {\n        database.withTransaction {\n            libraryEntryDao.updateSingle(libraryEntry)\n            libraryEntryModificationDao.deleteSingleMatchingCreateTime(\n                libraryEntryModification.id,\n                libraryEntryModification.createTime\n            )\n        }\n    }\n\n    suspend fun deleteLibraryEntryAndAnyModification(libraryEntryId: String) {\n        database.withTransaction {\n            libraryEntryDao.deleteSingleById(libraryEntryId)\n            libraryEntryModificationDao.deleteSingleById(libraryEntryId)\n        }\n    }\n\n    //********************************************************************************************//\n    // RemoteKey related methods\n    //********************************************************************************************//\n\n    suspend fun getRemoteKeyByResourceId(resourceId: String, remoteKeyType: RemoteKeyType) =\n        remoteKeyDao.getRemoteKeyByResourceId(resourceId, remoteKeyType)\n\n    suspend fun deleteRemoteKeyByResourceId(resourceId: String, remoteKeyType: RemoteKeyType) {\n        remoteKeyDao.deleteByResourceId(resourceId, remoteKeyType)\n    }\n\n    //********************************************************************************************//\n    // Utilities\n    //********************************************************************************************//\n\n    suspend fun <R> runDatabaseTransaction(block: suspend LocalDatabase.() -> R) =\n        database.withTransaction {\n            block(database)\n        }\n\n    /**\n     * Verifies that the library entry has an ID and contains a media object.\n     *\n     * @throws IllegalArgumentException if the library entry is not valid\n     */\n    private fun LocalLibraryEntry.verifyIsValidLibraryEntry() {\n        requireNotNull(this.id)\n        requireNotNull(this.media)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/LocalLibraryConverters.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library\n\nimport androidx.room.TypeConverter\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.common.media.AnimeSubtype\nimport io.github.drumber.kitsune.data.common.media.MangaSubtype\nimport io.github.drumber.kitsune.data.common.media.ReleaseStatus\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryStatus\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalReactionSkip\nimport io.github.drumber.kitsune.data.source.local.library.model.RemoteKeyType\nimport io.github.drumber.kitsune.di.createObjectMapper\n\nclass LocalLibraryConverters {\n\n    private val objectMapper = createObjectMapper()\n\n    @TypeConverter\n    fun stringListToString(list: List<String>?): String? {\n        return if (list != null) {\n            objectMapper.writeValueAsString(list)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringToStringList(listJson: String?): List<String>? {\n        return if (listJson != null) {\n            objectMapper.readValue(listJson)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringMapToString(map: Map<String, String?>?): String? {\n        return if (map != null) {\n            objectMapper.writeValueAsString(map)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringToStringMap(mapJson: String?): Map<String, String?>? {\n        return if (mapJson != null) {\n            objectMapper.readValue(mapJson)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun ageRatingToString(ageRating: AgeRating?): String? {\n        return if (ageRating != null) {\n            objectMapper.writeValueAsString(ageRating)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringToAgeRating(json: String?): AgeRating? {\n        return if (json != null) {\n            objectMapper.readValue(json)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun animeSubtypeToString(animeSubtype: AnimeSubtype?): String? {\n        return if (animeSubtype != null) {\n            objectMapper.writeValueAsString(animeSubtype)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringToAnimeSubtype(json: String?): AnimeSubtype? {\n        return if (json != null) {\n            objectMapper.readValue(json)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun mangaSubtypeToString(mangaSubtype: MangaSubtype?): String? {\n        return if (mangaSubtype != null) {\n            objectMapper.writeValueAsString(mangaSubtype)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringToMangaSubtype(json: String?): MangaSubtype? {\n        return if (json != null) {\n            objectMapper.readValue(json)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun statusToString(status: ReleaseStatus?): String? {\n        return if (status != null) {\n            objectMapper.writeValueAsString(status)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringToStatus(json: String?): ReleaseStatus? {\n        return if (json != null) {\n            objectMapper.readValue(json)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun libraryStatusToOrderId(status: LocalLibraryStatus?): Int? {\n        return status?.orderId\n    }\n\n    @TypeConverter\n    fun orderIdToLibraryStatus(orderId: Int?): LocalLibraryStatus? {\n        return if (orderId != null) {\n            LocalLibraryStatus.entries.find { it.orderId == orderId }\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun reactionSkipToString(reactionSkip: LocalReactionSkip?): String? {\n        return if (reactionSkip != null) {\n            objectMapper.writeValueAsString(reactionSkip)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringToReactionSkip(json: String?): LocalReactionSkip? {\n        return if (json != null) {\n            objectMapper.readValue(json)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun remoteKeyTypeToString(remoteKeyType: RemoteKeyType?): String? {\n        return if (remoteKeyType != null) {\n            objectMapper.writeValueAsString(remoteKeyType)\n        } else {\n            null\n        }\n    }\n\n    @TypeConverter\n    fun stringToRemoteKeyType(json: String?): RemoteKeyType? {\n        return if (json != null) {\n            objectMapper.readValue(json)\n        } else {\n            null\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/dao/LibraryEntryDao.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.dao\n\nimport androidx.lifecycle.LiveData\nimport androidx.paging.PagingSource\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.Update\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntry\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryMedia\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryStatus\n\n@Dao\ninterface LibraryEntryDao {\n\n    companion object {\n        /** Order by status orderId (see [LocalLibraryStatus]) and update time */\n        const val ORDER_BY_STATUS = \"ORDER BY status, DATETIME(updatedAt) DESC\"\n    }\n\n\n    /* ===================\n     * All Library Status\n     * =================== */\n\n    @Query(\"SELECT * FROM library_entries $ORDER_BY_STATUS\")\n    fun allLibraryEntriesPagingSource(): PagingSource<Int, LocalLibraryEntry>\n\n    @Query(\"SELECT * FROM library_entries WHERE media_type = :type $ORDER_BY_STATUS\")\n    fun allLibraryEntriesByTypePagingSource(type: LocalLibraryMedia.MediaType): PagingSource<Int, LocalLibraryEntry>\n\n\n    /* ===================\n     * Filtered by Status\n     * =================== */\n\n    @Query(\"SELECT * FROM library_entries WHERE status IN (:status) $ORDER_BY_STATUS\")\n    fun allLibraryEntriesByStatusPagingSource(status: List<LocalLibraryStatus>): PagingSource<Int, LocalLibraryEntry>\n\n    @Query(\"SELECT * FROM library_entries WHERE status IN (:status) AND media_type = :type $ORDER_BY_STATUS\")\n    fun allLibraryEntriesByTypeAndStatusPagingSource(type: LocalLibraryMedia.MediaType, status: List<LocalLibraryStatus>): PagingSource<Int, LocalLibraryEntry>\n\n\n    /* ===================\n     * Non Paging Queries\n     * =================== */\n\n    @Query(\"SELECT * FROM library_entries $ORDER_BY_STATUS\")\n    suspend fun getAllLibraryEntries(): List<LocalLibraryEntry>\n\n    @Query(\"SELECT * FROM library_entries WHERE media_type = :type $ORDER_BY_STATUS\")\n    suspend fun getAllLibraryEntriesByType(type: LocalLibraryMedia.MediaType): List<LocalLibraryEntry>\n\n    @Query(\"SELECT * FROM library_entries WHERE status IN (:status) $ORDER_BY_STATUS\")\n    suspend fun getAllLibraryEntriesByStatus(status: List<LocalLibraryStatus>): List<LocalLibraryEntry>\n\n    @Query(\"SELECT * FROM library_entries WHERE status IN (:status) AND media_type = :type $ORDER_BY_STATUS\")\n    suspend fun getAllLibraryEntriesByTypeAndStatus(type: LocalLibraryMedia.MediaType, status: List<LocalLibraryStatus>): List<LocalLibraryEntry>\n\n    @Query(\"SELECT * FROM library_entries WHERE media_id = :mediaId\")\n    suspend fun getLibraryEntryFromMedia(mediaId: String): LocalLibraryEntry?\n\n    @Query(\"SELECT * FROM library_entries WHERE media_id = :mediaId\")\n    fun getLibraryEntryFromMediaAsLiveData(mediaId: String): LiveData<LocalLibraryEntry?>\n\n    @Query(\"SELECT * FROM library_entries WHERE id = :id\")\n    suspend fun getLibraryEntry(id: String): LocalLibraryEntry?\n\n    @Query(\"SELECT EXISTS(SELECT 1 FROM library_entries WHERE id = :id AND DATETIME(updatedAt) > DATETIME(:updatedAt))\")\n    suspend fun hasLibraryEntryWhereUpdatedAtIsAfter(id: String, updatedAt: String): Boolean\n\n    @Query(\"SELECT * FROM library_entries WHERE id = :id\")\n    fun getLibraryEntryAsLiveData(id: String): LiveData<LocalLibraryEntry?>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(libraryEntry: List<LocalLibraryEntry>)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertSingle(libraryEntry: LocalLibraryEntry)\n\n    @Update\n    suspend fun updateSingle(libraryEntry: LocalLibraryEntry)\n\n    @Delete\n    suspend fun deleteSingle(libraryEntry: LocalLibraryEntry)\n\n    @Query(\"DELETE FROM library_entries WHERE id = :id\")\n    suspend fun deleteSingleById(id: String)\n\n    @Delete\n    suspend fun deleteAll(libraryEntries: List<LocalLibraryEntry>)\n\n    @Query(\"DELETE FROM library_entries\")\n    suspend fun clearLibraryEntries()\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/dao/LibraryEntryModificationDao.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.dao\n\nimport androidx.lifecycle.LiveData\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.Update\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntryModification\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryModificationState\nimport kotlinx.coroutines.flow.Flow\n\n@Dao\ninterface LibraryEntryModificationDao {\n\n    @Query(\"SELECT * FROM library_entries_modifications\")\n    fun getAllLibraryEntryModificationsAsFlow(): Flow<List<LocalLibraryEntryModification>>\n\n    @Query(\"SELECT * FROM library_entries_modifications WHERE state = :state\")\n    fun getLibraryEntryModificationsByStateAsLiveData(state: LocalLibraryModificationState): LiveData<List<LocalLibraryEntryModification>>\n\n    @Query(\"SELECT * FROM library_entries_modifications\")\n    suspend fun getAllLibraryEntryModifications(): List<LocalLibraryEntryModification>\n\n    @Query(\"SELECT * FROM library_entries_modifications WHERE id = :id\")\n    suspend fun getLibraryEntryModification(id: String): LocalLibraryEntryModification?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertSingle(libraryEntryModification: LocalLibraryEntryModification)\n\n    @Update\n    suspend fun updateSingle(libraryEntryModification: LocalLibraryEntryModification)\n\n    @Delete\n    suspend fun deleteSingle(libraryEntryModification: LocalLibraryEntryModification)\n\n    @Query(\"DELETE FROM library_entries_modifications WHERE id = :id\")\n    suspend fun deleteSingleById(id: String)\n\n    @Query(\"DELETE FROM library_entries_modifications WHERE id = :id AND createTime = :createTime\")\n    suspend fun deleteSingleMatchingCreateTime(id: String, createTime: Long)\n\n    @Query(\"DELETE FROM library_entries_modifications\")\n    suspend fun clearAll()\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/dao/LibraryEntryWithModificationDao.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.dao\n\nimport androidx.lifecycle.LiveData\nimport androidx.room.Dao\nimport androidx.room.Query\nimport androidx.room.Transaction\nimport io.github.drumber.kitsune.data.source.local.library.dao.LibraryEntryDao.Companion.ORDER_BY_STATUS\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntryWithModification\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryStatus\nimport kotlinx.coroutines.flow.Flow\n\n@Dao\ninterface LibraryEntryWithModificationDao {\n\n    @Transaction\n    @Query(\"SELECT * FROM library_entries WHERE media_id = :mediaId\")\n    suspend fun getLibraryEntryWithModificationFromMedia(mediaId: String): LocalLibraryEntryWithModification?\n\n    @Transaction\n    @Query(\"SELECT * FROM library_entries WHERE media_id = :mediaId\")\n    fun getLibraryEntryWithModificationFromMediaAsLiveData(mediaId: String): LiveData<LocalLibraryEntryWithModification?>\n\n    @Transaction\n    @Query(\"SELECT * FROM library_entries WHERE status IN (:status) $ORDER_BY_STATUS\")\n    suspend fun getLibraryEntriesWithModificationByStatus(status: List<LocalLibraryStatus>): List<LocalLibraryEntryWithModification>\n\n    @Transaction\n    @Query(\"SELECT * FROM library_entries WHERE status IN (:status) $ORDER_BY_STATUS\")\n    fun getLibraryEntriesWithModificationByStatusAsFlow(status: List<LocalLibraryStatus>): Flow<List<LocalLibraryEntryWithModification>>\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/dao/RemoteKeyDao.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport io.github.drumber.kitsune.data.source.local.library.model.RemoteKeyEntity\nimport io.github.drumber.kitsune.data.source.local.library.model.RemoteKeyType\n\n@Dao\ninterface RemoteKeyDao {\n\n    @Query(\"SELECT * FROM remote_keys WHERE resourceId = :resourceId AND remoteKeyType = :remoteKeyType\")\n    suspend fun getRemoteKeyByResourceId(resourceId: String, remoteKeyType: RemoteKeyType): RemoteKeyEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertALl(key: List<RemoteKeyEntity>)\n\n    @Query(\"DELETE FROM remote_keys WHERE resourceId = :resourceId AND remoteKeyType = :remoteKeyType\")\n    suspend fun deleteByResourceId(resourceId: String, remoteKeyType: RemoteKeyType)\n\n    @Query(\"DELETE FROM remote_keys WHERE remoteKeyType = :remoteKeyType AND resourceId IN (:resourceIds)\")\n    suspend fun deleteAllByResourceId(resourceIds: List<String>, remoteKeyType: RemoteKeyType)\n\n    @Query(\"DELETE FROM remote_keys WHERE remoteKeyType = :remoteKeyType\")\n    suspend fun clearRemoteKeys(remoteKeyType: RemoteKeyType)\n\n    @Query(\"DELETE FROM remote_keys\")\n    suspend fun clearAllRemoteKeys()\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/LocalImage.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nimport androidx.room.Embedded\n\ndata class LocalImage(\n    val tiny: String?,\n    val small: String?,\n    val medium: String?,\n    val large: String?,\n    val original: String?,\n    @Embedded(prefix = \"meta_\")\n    val meta: LocalImageMeta?\n)\n\ndata class LocalImageMeta(@Embedded val dimensions: LocalDimensions?)\n\ndata class LocalDimensions(\n    @Embedded(prefix = \"tiny_\")\n    val tiny: LocalDimension?,\n    @Embedded(prefix = \"small_\")\n    val small: LocalDimension?,\n    @Embedded(prefix = \"medium_\")\n    val medium: LocalDimension?,\n    @Embedded(prefix = \"large_\")\n    val large: LocalDimension?\n)\n\ndata class LocalDimension(val width: Int?, val height: Int?)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/LocalLibraryEntry.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nimport androidx.room.Embedded\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"library_entries\")\ndata class LocalLibraryEntry(\n    @PrimaryKey\n    val id: String,\n    val updatedAt: String?,\n\n    val startedAt: String?,\n    val finishedAt: String?,\n    val progressedAt: String?,\n\n    val status: LocalLibraryStatus?,\n    val progress: Int?,\n    val reconsuming: Boolean?,\n    val reconsumeCount: Int?,\n    val volumesOwned: Int?,\n    val ratingTwenty: Int?,\n\n    val notes: String?,\n    val privateEntry: Boolean?,\n    val reactionSkipped: LocalReactionSkip?,\n\n    @Embedded(\"media_\")\n    val media: LocalLibraryMedia?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/LocalLibraryEntryModification.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryModificationState.NOT_SYNCHRONIZED\n\n@Entity(tableName = \"library_entries_modifications\")\ndata class LocalLibraryEntryModification(\n    /** Corresponds to the library entry ID */\n    @PrimaryKey val id: String,\n\n    val createTime: Long = System.currentTimeMillis(),\n    val state: LocalLibraryModificationState = NOT_SYNCHRONIZED,\n\n    val startedAt: String?,\n    val finishedAt: String?,\n\n    val status: LocalLibraryStatus?,\n    val progress: Int?,\n    val reconsumeCount: Int?,\n    val volumesOwned: Int?,\n    /**  Set to `-1` to remove rating (will be mapped to `null` by the json serializer) */\n    val ratingTwenty: Int?,\n\n    val notes: String?,\n    val privateEntry: Boolean?\n) {\n\n    fun isEqualToLibraryEntry(localLibraryEntry: LocalLibraryEntry): Boolean {\n        return id == localLibraryEntry.id && applyToLibraryEntry(localLibraryEntry) == localLibraryEntry\n    }\n\n    fun applyToLibraryEntry(localLibraryEntry: LocalLibraryEntry): LocalLibraryEntry {\n        require(id == localLibraryEntry.id) { \"ID of the library modification and the entry must be the same.\" }\n        return localLibraryEntry.copy(\n            startedAt = startedAt ?: localLibraryEntry.startedAt,\n            finishedAt = finishedAt ?: localLibraryEntry.finishedAt,\n            status = status ?: localLibraryEntry.status,\n            progress = progress ?: localLibraryEntry.progress,\n            reconsumeCount = reconsumeCount ?: localLibraryEntry.reconsumeCount,\n            volumesOwned = volumesOwned ?: localLibraryEntry.volumesOwned,\n            ratingTwenty = ratingTwenty ?: localLibraryEntry.ratingTwenty,\n            notes = notes?.takeIf { it.isNotBlank() || !localLibraryEntry.notes.isNullOrBlank() }\n                ?: localLibraryEntry.notes,\n            privateEntry = privateEntry ?: localLibraryEntry.privateEntry\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/LocalLibraryEntryWithModification.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nimport androidx.room.Embedded\nimport androidx.room.Relation\n\ndata class LocalLibraryEntryWithModification(\n    @Embedded\n    val libraryEntry: LocalLibraryEntry,\n    @Relation(\n        parentColumn = \"id\",\n        entityColumn = \"id\"\n    )\n    val libraryEntryModification: LocalLibraryEntryModification?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/LocalLibraryMedia.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nimport androidx.room.Embedded\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.common.media.AnimeSubtype\nimport io.github.drumber.kitsune.data.common.media.MangaSubtype\nimport io.github.drumber.kitsune.data.common.media.RatingFrequencies\nimport io.github.drumber.kitsune.data.common.media.ReleaseStatus\n\ndata class LocalLibraryMedia(\n    val id: String,\n    val type: MediaType,\n\n    val description: String?,\n    val titles: Titles?,\n    val canonicalTitle: String?,\n    val abbreviatedTitles: List<String>?,\n\n    val averageRating: String?,\n    @Embedded(prefix = \"rating_\")\n    val ratingFrequencies: RatingFrequencies?,\n    val popularityRank: Int?,\n    val ratingRank: Int?,\n\n    val startDate: String?,\n    val endDate: String?,\n    val nextRelease: String?,\n    val tba: String?,\n    val status: ReleaseStatus?,\n\n    val ageRating: AgeRating?,\n    val ageRatingGuide: String?,\n    val nsfw: Boolean?,\n\n    @Embedded(prefix = \"poster_\")\n    val posterImage: LocalImage?,\n    @Embedded(prefix = \"cover_\")\n    val coverImage: LocalImage?,\n\n    // Anime specific attributes\n    val animeSubtype: AnimeSubtype?,\n    val totalLength: Int?,\n    val episodeCount: Int?,\n    val episodeLength: Int?,\n\n    // Manga specific attributes\n    val mangaSubtype: MangaSubtype?,\n    val chapterCount: Int?,\n    val volumeCount: Int?,\n    val serialization: String?\n) {\n    enum class MediaType {\n        Anime,\n        Manga\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/LocalLibraryModificationState.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nenum class LocalLibraryModificationState {\n    SYNCHRONIZING,\n    NOT_SYNCHRONIZED\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/LocalLibraryStatus.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nenum class LocalLibraryStatus(val orderId: Int) {\n    Current(0),\n    Planned(1),\n    Completed(2),\n    OnHold(3),\n    Dropped(4)\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/LocalReactionSkip.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nenum class LocalReactionSkip {\n    Unskipped,\n    Skipped,\n    Ignored\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/library/model/RemoteKeyEntity.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"remote_keys\")\ndata class RemoteKeyEntity(\n    @PrimaryKey\n    @ColumnInfo(collate = ColumnInfo.NOCASE)\n    val resourceId: String,\n    val remoteKeyType: RemoteKeyType,\n    val prevPageKey: Int?,\n    val nextPageKey: Int?\n)\n\nenum class RemoteKeyType {\n    LibraryEntry\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/user/UserLocalDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.user\n\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\n\ninterface UserLocalDataSource {\n\n    fun loadUser(): LocalUser?\n\n    fun storeUser(user: LocalUser)\n\n    fun clearUser()\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/user/UserPreferences.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.user\n\nimport android.content.Context\nimport androidx.core.content.edit\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.util.logD\n\nclass UserPreferences(context: Context, private val objectMapper: ObjectMapper) : UserLocalDataSource {\n\n    private val sharedPreferences = context.getSharedPreferences(\n        context.getString(R.string.user_preference_file_key),\n        Context.MODE_PRIVATE\n    )\n\n    override fun loadUser(): LocalUser? {\n        val jsonString = sharedPreferences.getString(KEY_USER_MODEL, null)\n        return if (!jsonString.isNullOrBlank()) {\n            logD(\"Parse and return user model stored as json string.\")\n            return objectMapper.readValue(jsonString)\n        } else {\n            logD(\"No user model stored.\")\n            null\n        }\n    }\n\n    override fun storeUser(user: LocalUser) {\n        logD(\"Storing user model in shared preferences.\")\n        val jsonString = objectMapper.writeValueAsString(user)\n        sharedPreferences.edit {\n            putString(KEY_USER_MODEL, jsonString)\n        }\n    }\n\n    override fun clearUser() {\n        logD(\"Deleting user model from shared preferences.\")\n        sharedPreferences.edit {\n            remove(KEY_USER_MODEL)\n        }\n    }\n\n    companion object {\n        const val KEY_USER_MODEL = \"user_model\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/user/model/LocalRatingSystemPreference.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.user.model\n\nenum class LocalRatingSystemPreference {\n    // 0.5, 1...10\n    Advanced,\n    // 0.5, 1...5\n    Regular,\n    // :(, :|, :), :D\n    Simple\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/user/model/LocalSfwFilterPreference.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.user.model\n\nenum class LocalSfwFilterPreference {\n    SFW,\n    NSFW_SOMETIMES,\n    NSFW_EVERYWHERE\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/user/model/LocalTitleLanguagePreference.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.user.model\n\nenum class LocalTitleLanguagePreference {\n    Canonical,\n    Romanized,\n    English\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/local/user/model/LocalUser.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.user.model\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.user.UserThemePreference\nimport io.github.drumber.kitsune.data.source.local.character.LocalCharacter\n\ndata class LocalUser(\n    val id: String,\n    val createdAt: String?,\n\n    val name: String?,\n    val slug: String?,\n    val email: String?,\n    val title: String?,\n\n    val avatar: Image?,\n    val coverImage: Image?,\n\n    val about: String?,\n    val location: String?,\n    val gender: String?,\n    val birthday: String?,\n    val waifuOrHusbando: String?,\n\n    val followersCount: Int?,\n    val followingCount: Int?,\n\n    val country: String?,\n    val language: String?,\n    val timeZone: String?,\n    val theme: UserThemePreference?,\n\n    val sfwFilter: Boolean?,\n    val ratingSystem: LocalRatingSystemPreference?,\n    val sfwFilterPreference: LocalSfwFilterPreference?,\n    val titleLanguagePreference: LocalTitleLanguagePreference?,\n\n    val waifu: LocalCharacter?\n) {\n    companion object {\n        fun empty(id: String) = LocalUser(\n            id = id,\n            createdAt = null,\n            name = null,\n            slug = null,\n            email = null,\n            title = null,\n            avatar = null,\n            coverImage = null,\n            about = null,\n            location = null,\n            gender = null,\n            birthday = null,\n            waifuOrHusbando = null,\n            followersCount = null,\n            followingCount = null,\n            country = null,\n            language = null,\n            timeZone = null,\n            theme = null,\n            sfwFilter = null,\n            ratingSystem = null,\n            sfwFilterPreference = null,\n            titleLanguagePreference = null,\n            waifu = null\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/BasePagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network\n\nimport androidx.paging.PagingSource\nimport androidx.paging.PagingState\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.util.logE\n\nabstract class BasePagingDataSource<Value : Any> : PagingSource<Int, Value>() {\n\n    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {\n        return try {\n            val pageOffset = params.key ?: Kitsu.DEFAULT_PAGE_OFFSET\n            val pageData = requestPage(pageOffset)\n\n            val data = pageData.data ?: throw NoDataException(\"Received data is 'null'.\")\n\n            LoadResult.Page(\n                data = data,\n                prevKey = pageData.prev,\n                nextKey = pageData.next\n            )\n        } catch (e: Exception) {\n            logE(\"Error receiving data from API.\", e)\n            LoadResult.Error(e)\n        }\n    }\n\n    abstract suspend fun requestPage(pageOffset: Int): PageData<Value>\n\n    override fun getRefreshKey(state: PagingState<Int, Value>) =\n        state.anchorPosition?.let { anchorPosition ->\n            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)\n                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)\n        }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/PageData.kt",
    "content": "package io.github.drumber.kitsune.data.source.network\n\nimport android.net.Uri\nimport com.github.jasminb.jsonapi.JSONAPIDocument\n\ndata class PageData<Value : Any>(\n    val data: List<Value>?,\n    val first: Int?,\n    val last: Int?,\n    val prev: Int?,\n    val next: Int?\n)\n\nfun <Value : Any> JSONAPIDocument<List<Value>>.toPageData() = PageData(\n    data = get(),\n    first = links?.first?.href?.parseOffset(),\n    last = links?.last?.href?.parseOffset(),\n    next = links?.next?.href?.parseOffset(),\n    prev = links?.previous?.href?.parseOffset()\n)\n\nprivate fun String.parseOffset() =\n    Uri.parse(this).getQueryParameter(\"page[offset]\")?.toInt()\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/algolia/AlgoliaKeyNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.algolia\n\nimport io.github.drumber.kitsune.data.source.network.algolia.api.AlgoliaKeyApi\nimport io.github.drumber.kitsune.data.source.network.algolia.model.NetworkAlgoliaKeyCollection\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass AlgoliaKeyNetworkDataSource(\n    private val algoliaKeyApi: AlgoliaKeyApi\n) {\n\n    suspend fun getAllAlgoliaKeys(): NetworkAlgoliaKeyCollection {\n        return withContext(Dispatchers.IO) {\n            algoliaKeyApi.getAllAlgoliaKeys()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/algolia/api/AlgoliaKeyApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.algolia.api\n\nimport io.github.drumber.kitsune.data.source.network.algolia.model.NetworkAlgoliaKeyCollection\nimport retrofit2.http.GET\n\ninterface AlgoliaKeyApi {\n\n    @GET(\"algolia-keys\")\n    suspend fun getAllAlgoliaKeys(): NetworkAlgoliaKeyCollection\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/algolia/model/NetworkAlgoliaKey.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.algolia.model\n\ndata class NetworkAlgoliaKey(\n    val key: String?,\n    val index: String?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/algolia/model/NetworkAlgoliaKeyCollection.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.algolia.model\n\ndata class NetworkAlgoliaKeyCollection(\n    val users: NetworkAlgoliaKey?,\n    val posts: NetworkAlgoliaKey?,\n    val media: NetworkAlgoliaKey?,\n    val groups: NetworkAlgoliaKey?,\n    val characters: NetworkAlgoliaKey?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/algolia/model/search/AlgoliaCharacterSearchResult.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.algolia.model.search\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AlgoliaCharacterSearchResult(\n    val id: Long,\n    val slug: String? = null,\n    val canonicalName: String? = null,\n    val image: AlgoliaImage? = null,\n    val primaryMedia: String? = null\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/algolia/model/search/AlgoliaImage.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.algolia.model.search\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AlgoliaImage(\n    val tiny: String? = null,\n    val small: String? = null,\n    val medium: String? = null,\n    val large: String? = null,\n    val original: String? = null,\n    val meta: AlgoliaImageMeta? = null,\n)\n\n@Serializable\ndata class AlgoliaImageMeta(val dimensions: AlgoliaDimensions? = null)\n\n@Serializable\ndata class AlgoliaDimensions(\n    val tiny: AlgoliaDimension? = null,\n    val small: AlgoliaDimension? = null,\n    val medium: AlgoliaDimension? = null,\n    val large: AlgoliaDimension? = null\n)\n\n@Serializable\ndata class AlgoliaDimension(val width: Int? = null, val height: Int? = null)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/algolia/model/search/AlgoliaMediaSearchKind.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.algolia.model.search\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\nenum class AlgoliaMediaSearchKind {\n    @SerialName(\"anime\")\n    Anime,\n    @SerialName(\"manga\")\n    Manga\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/algolia/model/search/AlgoliaMediaSearchResult.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.algolia.model.search\n\nimport io.github.drumber.kitsune.data.common.Titles\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AlgoliaMediaSearchResult(\n    val id: Long,\n    val kind: AlgoliaMediaSearchKind,\n    val subtype: String? = null,\n    val slug: String? = null,\n    val titles: Titles? = null,\n    val canonicalTitle: String? = null,\n    val posterImage: AlgoliaImage? = null\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/appupdate/AppReleaseNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.appupdate\n\nimport io.github.drumber.kitsune.data.source.network.appupdate.api.GitHubApi\nimport io.github.drumber.kitsune.data.source.network.appupdate.model.NetworkGitHubRelease\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass AppReleaseNetworkDataSource(\n    private val service: GitHubApi\n) {\n\n    suspend fun getLatestRelease(): NetworkGitHubRelease {\n        return withContext(Dispatchers.IO) {\n            service.getLatestRelease()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/appupdate/api/GitHubApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.appupdate.api\n\nimport io.github.drumber.kitsune.data.source.network.appupdate.model.NetworkGitHubRelease\nimport retrofit2.http.GET\n\ninterface GitHubApi {\n\n    @GET(\"releases/latest\")\n    suspend fun getLatestRelease(): NetworkGitHubRelease\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/appupdate/model/NetworkGitHubRelease.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.appupdate.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\ndata class NetworkGitHubRelease(\n    @JsonProperty(\"tag_name\")\n    val version: String,\n    @JsonProperty(\"html_url\")\n    val url: String,\n    @JsonProperty(\"published_at\")\n    val publishDate: String\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/auth/AccessTokenNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.auth\n\nimport io.github.drumber.kitsune.data.source.network.auth.api.AuthenticationApi\nimport io.github.drumber.kitsune.data.source.network.auth.model.NetworkAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.ObtainAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.RefreshAccessToken\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass AccessTokenNetworkDataSource(\n    private val authenticationApi: AuthenticationApi\n) {\n\n    suspend fun obtainAccessToken(obtainAccessToken: ObtainAccessToken): NetworkAccessToken {\n        return withContext(Dispatchers.IO) {\n            authenticationApi.obtainAccessToken(obtainAccessToken)\n        }\n    }\n\n    suspend fun refreshToken(refreshAccessToken: RefreshAccessToken): NetworkAccessToken {\n        return withContext(Dispatchers.IO) {\n            authenticationApi.refreshToken(refreshAccessToken)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/auth/api/AuthenticationApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.auth.api\n\nimport io.github.drumber.kitsune.data.source.network.auth.model.NetworkAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.ObtainAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.RefreshAccessToken\nimport retrofit2.http.Body\nimport retrofit2.http.POST\n\ninterface AuthenticationApi {\n\n    @POST(\"token\")\n    suspend fun obtainAccessToken(\n        @Body obtainAccessToken: ObtainAccessToken\n    ): NetworkAccessToken\n\n    @POST(\"token\")\n    suspend fun refreshToken(\n        @Body refreshAccessToken: RefreshAccessToken\n    ): NetworkAccessToken\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/auth/model/NetworkAccessToken.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.auth.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\n/**\n * @param createdAt time in seconds the access token was created\n * @param expiresIn seconds until the [accessToken] expires (30 days default)\n */\ndata class NetworkAccessToken(\n    @JsonProperty(\"access_token\") val accessToken: String?,\n    @JsonProperty(\"created_at\") val createdAt: Long?,\n    @JsonProperty(\"expires_in\") val expiresIn: Long?,\n    @JsonProperty(\"refresh_token\") val refreshToken: String?,\n    @JsonProperty(\"scope\") val scope: String?,\n    @JsonProperty(\"token_type\") val tokenType: String?,\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/auth/model/ObtainAccessToken.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.auth.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\ndata class ObtainAccessToken(\n    @JsonProperty(\"grant_type\")\n    val grantType: String = \"password\",\n    @JsonProperty(\"username\")\n    val username: String,\n    @JsonProperty(\"password\")\n    val password: String\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/auth/model/RefreshAccessToken.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.auth.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\ndata class RefreshAccessToken(\n    @JsonProperty(\"grant_type\")\n    val grantType: String = \"refresh_token\",\n    @JsonProperty(\"refresh_token\")\n    val refreshToken: String\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/character/CharacterNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.character\n\nimport io.github.drumber.kitsune.data.source.network.character.api.CharacterApi\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass CharacterNetworkDataSource(\n    private val characterApi: CharacterApi\n) {\n\n    suspend fun getCharacter(id: String, filter: Filter): NetworkCharacter? {\n        return withContext(Dispatchers.IO) {\n            characterApi.getCharacter(id, filter.options).get()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/character/api/CharacterApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.character.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface CharacterApi {\n\n    @GET(\"characters/{id}\")\n    suspend fun getCharacter(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkCharacter>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/character/model/NetworkCharacter.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.character.model\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavoriteItem\n\n@Type(\"characters\")\ndata class NetworkCharacter(\n    @Id\n    val id: String?,\n    val slug: String? = null,\n    val name: String? = null,\n    val names: Titles? = null,\n    val otherNames: List<String>? = null,\n    val malId: Int? = null,\n    val description: String? = null,\n    val image: Image? = null,\n\n    @Relationship(\"mediaCharacters\")\n    val mediaCharacters: List<NetworkMediaCharacter>? = null\n) : NetworkFavoriteItem\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/character/model/NetworkMediaCharacter.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.character.model\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkMedia\n\n@Type(\"mediaCharacters\")\ndata class NetworkMediaCharacter(\n    @Id\n    val id: String?,\n    val role: NetworkMediaCharacterRole?,\n\n    @Relationship(\"media\")\n    val media: NetworkMedia?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/character/model/NetworkMediaCharacterRole.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.character.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkMediaCharacterRole {\n    @JsonProperty(\"main\")\n    MAIN,\n    @JsonProperty(\"supporting\")\n    SUPPORTING,\n    @JsonProperty(\"recurring\")\n    RECURRING,\n    @JsonProperty(\"cameo\")\n    CAMEO\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/library/LibraryEntryPagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.library\n\nimport io.github.drumber.kitsune.data.source.network.BasePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryEntry\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass LibraryEntryPagingDataSource(\n    private val dataSource: LibraryNetworkDataSource,\n    private val filter: Filter\n) : BasePagingDataSource<NetworkLibraryEntry>() {\n\n    override suspend fun requestPage(pageOffset: Int): PageData<NetworkLibraryEntry> {\n        return dataSource.getAllLibraryEntries(filter.pageOffset(pageOffset))\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/library/LibraryNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.library\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.library.api.LibraryEntryApi\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryEntry\nimport io.github.drumber.kitsune.data.source.network.toPageData\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass LibraryNetworkDataSource(\n    private val libraryEntryApi: LibraryEntryApi\n) {\n\n    suspend fun getAllLibraryEntries(filter: Filter): PageData<NetworkLibraryEntry> {\n        return withContext(Dispatchers.IO) {\n            libraryEntryApi.getAllLibraryEntries(filter.options).toPageData()\n        }\n    }\n\n    suspend fun getLibraryEntry(id: String, filter: Filter): NetworkLibraryEntry? {\n        return withContext(Dispatchers.IO) {\n            libraryEntryApi.getLibraryEntry(id, filter.options).get()\n        }\n    }\n\n    suspend fun updateLibraryEntry(\n        id: String,\n        libraryEntry: NetworkLibraryEntry,\n        filter: Filter = Filter()\n    ): NetworkLibraryEntry? {\n        return withContext(Dispatchers.IO) {\n            libraryEntryApi.updateLibraryEntry(\n                id,\n                JSONAPIDocument(libraryEntry),\n                filter.options\n            ).get()\n        }\n    }\n\n    suspend fun postLibraryEntry(\n        libraryEntry: NetworkLibraryEntry,\n        filter: Filter = Filter()\n    ): NetworkLibraryEntry? {\n        return withContext(Dispatchers.IO) {\n            libraryEntryApi.postLibraryEntry(JSONAPIDocument(libraryEntry), filter.options).get()\n        }\n    }\n\n    suspend fun deleteLibraryEntry(id: String) {\n        return withContext(Dispatchers.IO) {\n            libraryEntryApi.deleteLibraryEntry(id)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/library/api/LibraryEntryApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.library.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryEntry\nimport retrofit2.Response\nimport retrofit2.http.Body\nimport retrofit2.http.DELETE\nimport retrofit2.http.GET\nimport retrofit2.http.PATCH\nimport retrofit2.http.POST\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface LibraryEntryApi {\n\n    @GET(\"library-entries\")\n    suspend fun getAllLibraryEntries(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkLibraryEntry>>\n\n    @GET(\"library-entries/{id}\")\n    suspend fun getLibraryEntry(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkLibraryEntry>\n\n    @PATCH(\"library-entries/{id}\")\n    suspend fun updateLibraryEntry(\n        @Path(\"id\") id: String,\n        @Body libraryEntry: JSONAPIDocument<NetworkLibraryEntry>,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkLibraryEntry>\n\n    @POST(\"library-entries\")\n    suspend fun postLibraryEntry(\n        @Body libraryEntry: JSONAPIDocument<NetworkLibraryEntry>,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkLibraryEntry>\n\n    @DELETE(\"library-entries/{id}\")\n    suspend fun deleteLibraryEntry(\n        @Path(\"id\") id: String\n    ): Response<Unit>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/library/model/NetworkLibraryEntry.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.library.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\nimport io.github.drumber.kitsune.util.json.NullableIntSerializer\n\n@Type(\"libraryEntries\")\ndata class NetworkLibraryEntry(\n    @Id\n    val id: String?,\n    val updatedAt: String?,\n\n    val startedAt: String?,\n    val finishedAt: String?,\n    val progressedAt: String?,\n\n    val status: NetworkLibraryStatus?,\n    val progress: Int?,\n    val reconsuming: Boolean?,\n    val reconsumeCount: Int?,\n    val volumesOwned: Int?,\n    /** set ratingTwenty to '-1' to serialize to 'null' */\n    @JsonSerialize(using = NullableIntSerializer::class)\n    val ratingTwenty: Int?,\n\n    val notes: String?,\n    @JsonProperty(\"private\")\n    val privateEntry: Boolean?,\n    val reactionSkipped: NetworkReactionSkip?,\n\n    @Relationship(\"anime\")\n    val anime: NetworkAnime?,\n    @Relationship(\"manga\")\n    val manga: NetworkManga?,\n    @Relationship(\"user\")\n    val user: NetworkUser?\n) {\n\n    companion object {\n        fun new(\n            userId: String,\n            mediaType: MediaType,\n            mediaId: String,\n            status: NetworkLibraryStatus? = null\n        ) = NetworkLibraryEntry(\n            user = NetworkUser(id = userId),\n            status = status,\n            anime = mediaType.takeIf { it == MediaType.Anime }?.let { NetworkAnime.empty(mediaId) },\n            manga = mediaType.takeIf { it == MediaType.Manga }?.let { NetworkManga.empty(mediaId) },\n            id = null,\n            updatedAt = null,\n            startedAt = null,\n            finishedAt = null,\n            progressedAt = null,\n            progress = null,\n            reconsuming = null,\n            reconsumeCount = null,\n            volumesOwned = null,\n            ratingTwenty = null,\n            notes = null,\n            privateEntry = null,\n            reactionSkipped = null\n        )\n\n        fun update(\n            id: String,\n            startedAt: String? = null,\n            finishedAt: String? = null,\n            status: NetworkLibraryStatus? = null,\n            progress: Int? = null,\n            reconsumeCount: Int? = null,\n            volumesOwned: Int? = null,\n            ratingTwenty: Int? = null,\n            notes: String? = null,\n            isPrivate: Boolean? = null\n        ) = NetworkLibraryEntry(\n            id = id,\n            updatedAt = null,\n            startedAt = startedAt,\n            finishedAt = finishedAt,\n            progressedAt = null,\n            status = status,\n            progress = progress,\n            reconsuming = null,\n            reconsumeCount = reconsumeCount,\n            volumesOwned = volumesOwned,\n            ratingTwenty = ratingTwenty,\n            notes = notes,\n            privateEntry = isPrivate,\n            reactionSkipped = null,\n            anime = null,\n            manga = null,\n            user = null\n        )\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/library/model/NetworkLibraryStatus.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.library.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkLibraryStatus {\n    @JsonProperty(\"current\")\n    Current,\n\n    @JsonProperty(\"planned\")\n    Planned,\n\n    @JsonProperty(\"completed\")\n    Completed,\n\n    @JsonProperty(\"on_hold\")\n    OnHold,\n\n    @JsonProperty(\"dropped\")\n    Dropped\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/library/model/NetworkReactionSkip.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.library.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkReactionSkip {\n    @JsonProperty(\"unskipped\")\n    Unskipped,\n\n    @JsonProperty(\"skipped\")\n    Skipped,\n\n    @JsonProperty(\"ignored\")\n    Ignored\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/mapping/MappingNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.mapping\n\nimport io.github.drumber.kitsune.data.source.network.mapping.api.MappingApi\nimport io.github.drumber.kitsune.data.source.network.mapping.model.NetworkMapping\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass MappingNetworkDataSource(\n    private val mappingApi: MappingApi\n) {\n\n    suspend fun getAnimeMappings(animeId: String, filter: Filter): List<NetworkMapping>? {\n        return withContext(Dispatchers.IO) {\n            mappingApi.getAnimeMappings(animeId, filter.options).get()\n        }\n    }\n\n    suspend fun getMangaMappings(mangaId: String, filter: Filter): List<NetworkMapping>? {\n        return withContext(Dispatchers.IO) {\n            mappingApi.getMangaMappings(mangaId, filter.options).get()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/mapping/api/MappingApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.mapping.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.mapping.model.NetworkMapping\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface MappingApi {\n\n    @GET(\"anime/{id}/mappings\")\n    suspend fun getAnimeMappings(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkMapping>>\n\n    @GET(\"manga/{id}/mappings\")\n    suspend fun getMangaMappings(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkMapping>>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/mapping/model/NetworkMapping.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.mapping.model\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"mappings\")\ndata class NetworkMapping(\n    @Id\n    val id: String?,\n    val externalSite: String?,\n    val externalId: String?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/AnimeNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.api.AnimeApi\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.source.network.toPageData\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass AnimeNetworkDataSource(\n    private val animeApi: AnimeApi\n) {\n\n    suspend fun getAllAnime(filter: Filter): PageData<NetworkAnime> {\n        return withContext(Dispatchers.IO) {\n            animeApi.getAllAnime(filter.options).toPageData()\n        }\n    }\n\n    suspend fun getAnime(id: String, filter: Filter): NetworkAnime? {\n        return withContext(Dispatchers.IO) {\n            animeApi.getAnime(id, filter.options).get()\n        }\n    }\n\n    suspend fun getTrending(filter: Filter): PageData<NetworkAnime> {\n        return withContext(Dispatchers.IO) {\n            animeApi.getTrending(filter.options).toPageData()\n        }\n    }\n\n    suspend fun getLanguages(id: String): List<String> {\n        return withContext(Dispatchers.IO) {\n            animeApi.getLanguages(id)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/AnimePagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.BasePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass AnimePagingDataSource(\n    private val dataSource: AnimeNetworkDataSource,\n    private val filter: Filter\n) : BasePagingDataSource<NetworkAnime>() {\n\n    override suspend fun requestPage(pageOffset: Int): PageData<NetworkAnime> {\n        return dataSource.getAllAnime(filter.pageOffset(pageOffset))\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/CastingNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.api.CastingApi\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkCasting\nimport io.github.drumber.kitsune.data.source.network.toPageData\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass CastingNetworkDataSource(\n    private val castingApi: CastingApi\n) {\n\n    suspend fun getAllCastings(filter: Filter): PageData<NetworkCasting> {\n        return withContext(Dispatchers.IO) {\n            castingApi.getAllCastings(filter.options).toPageData()\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/CastingPagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.BasePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkCasting\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass CastingPagingDataSource(\n    private val dataSource: CastingNetworkDataSource,\n    private val filter: Filter\n) : BasePagingDataSource<NetworkCasting>() {\n\n    override suspend fun requestPage(pageOffset: Int): PageData<NetworkCasting> {\n        return dataSource.getAllCastings(filter.pageOffset(pageOffset))\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/CategoryNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.media.api.CategoryApi\nimport io.github.drumber.kitsune.data.source.network.media.model.category.NetworkCategory\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass CategoryNetworkDataSource(\n    private val categoryApi: CategoryApi\n) {\n\n    suspend fun getAllCategories(filter: Filter): List<NetworkCategory>? {\n        return withContext(Dispatchers.IO) {\n            categoryApi.getAllCategories(filter.options).get()\n        }\n    }\n\n    suspend fun getCategory(id: String, filter: Filter): NetworkCategory? {\n        return withContext(Dispatchers.IO) {\n            categoryApi.getCategory(id, filter.options).get()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/ChapterNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.api.ChapterApi\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkChapter\nimport io.github.drumber.kitsune.data.source.network.toPageData\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass ChapterNetworkDataSource(\n    private val chapterApi: ChapterApi\n) {\n\n    suspend fun getAllChapters(filter: Filter): PageData<NetworkChapter> {\n        return chapterApi.getAllChapters(filter.options).toPageData()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/ChapterPagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.BasePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkChapter\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass ChapterPagingDataSource(\n    private val dataSource: ChapterNetworkDataSource,\n    private val filter: Filter\n) : BasePagingDataSource<NetworkChapter>() {\n\n    override suspend fun requestPage(pageOffset: Int): PageData<NetworkChapter> {\n        return dataSource.getAllChapters(filter.pageOffset(pageOffset))\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/EpisodeNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.api.EpisodeApi\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkEpisode\nimport io.github.drumber.kitsune.data.source.network.toPageData\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass EpisodeNetworkDataSource(\n    private val episodeApi: EpisodeApi\n) {\n\n    suspend fun getAllEpisodes(filter: Filter): PageData<NetworkEpisode> {\n        return episodeApi.getAllEpisodes(filter.options).toPageData()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/EpisodePagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.BasePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkEpisode\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass EpisodePagingDataSource(\n    private val dataSource: EpisodeNetworkDataSource,\n    private val filter: Filter\n) : BasePagingDataSource<NetworkEpisode>() {\n\n    override suspend fun requestPage(pageOffset: Int): PageData<NetworkEpisode> {\n        return dataSource.getAllEpisodes(filter.pageOffset(pageOffset))\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/MangaNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.api.MangaApi\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.source.network.toPageData\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass MangaNetworkDataSource(\n    private val mangaApi: MangaApi\n) {\n\n    suspend fun getAllManga(filter: Filter): PageData<NetworkManga> {\n        return withContext(Dispatchers.IO) {\n            mangaApi.getAllManga(filter.options).toPageData()\n        }\n    }\n\n    suspend fun getManga(id: String, filter: Filter): NetworkManga? {\n        return withContext(Dispatchers.IO) {\n            mangaApi.getManga(id, filter.options).get()\n        }\n    }\n\n    suspend fun getTrending(filter: Filter): PageData<NetworkManga> {\n        return withContext(Dispatchers.IO) {\n            mangaApi.getTrending(filter.options).toPageData()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/MangaPagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.BasePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass MangaPagingDataSource(\n    private val dataSource: MangaNetworkDataSource,\n    private val filter: Filter\n) : BasePagingDataSource<NetworkManga>() {\n\n    override suspend fun requestPage(pageOffset: Int): PageData<NetworkManga> {\n        return dataSource.getAllManga(filter.pageOffset(pageOffset))\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/TrendingAnimePagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.BasePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass TrendingAnimePagingDataSource(\n    private val dataSource: AnimeNetworkDataSource,\n    private val filter: Filter\n) : BasePagingDataSource<NetworkAnime>() {\n\n    override suspend fun requestPage(pageOffset: Int): PageData<NetworkAnime> {\n        return dataSource.getTrending(filter.pageOffset(pageOffset))\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/TrendingMangaPagingDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media\n\nimport io.github.drumber.kitsune.data.source.network.BasePagingDataSource\nimport io.github.drumber.kitsune.data.source.network.PageData\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.common.Filter\n\nclass TrendingMangaPagingDataSource(\n    private val dataSource: MangaNetworkDataSource,\n    private val filter: Filter\n) : BasePagingDataSource<NetworkManga>() {\n\n    override suspend fun requestPage(pageOffset: Int): PageData<NetworkManga> {\n        return dataSource.getTrending(filter.pageOffset(pageOffset))\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/api/AnimeApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface AnimeApi {\n\n    @GET(\"anime\")\n    suspend fun getAllAnime(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkAnime>>\n\n    @GET(\"anime/{id}\")\n    suspend fun getAnime(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkAnime>\n\n    @GET(\"trending/anime\")\n    suspend fun getTrending(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkAnime>>\n\n    // Will probably be replaced by origin_languages attribute in anime and manga\n    // see: https://github.com/hummingbird-me/kitsu-server/commit/e730ef2e0482d37e7252496c9e937c3e1164bf08\n    @GET(\"anime/{id}/_languages\")\n    suspend fun getLanguages(\n        @Path(\"id\") id: String\n    ): List<String>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/api/CastingApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkCasting\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface CastingApi {\n\n    @GET(\"castings\")\n    suspend fun getAllCastings(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkCasting>>\n\n    @GET(\"castings/{id}\")\n    suspend fun getCasting(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkCasting>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/api/CategoryApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.media.model.category.NetworkCategory\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface CategoryApi {\n\n    @GET(\"categories\")\n    suspend fun getAllCategories(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkCategory>>\n\n    @GET(\"categories/{id}\")\n    suspend fun getCategory(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkCategory>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/api/ChapterApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkChapter\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface ChapterApi {\n\n    @GET(\"chapters\")\n    suspend fun getAllChapters(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkChapter>>\n\n    @GET(\"chapters/{id}\")\n    suspend fun getChapter(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkChapter>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/api/EpisodeApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkEpisode\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface EpisodeApi {\n\n    @GET(\"episodes\")\n    suspend fun getAllEpisodes(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkEpisode>>\n\n    @GET(\"episodes/{id}\")\n    suspend fun getEpisode(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkEpisode>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/api/MangaApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface MangaApi {\n\n    @GET(\"manga\")\n    suspend fun getAllManga(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkManga>>\n\n    @GET(\"manga/{id}\")\n    suspend fun getManga(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkManga>\n\n    @GET(\"trending/manga\")\n    suspend fun getTrending(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkManga>>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/NetworkAnime.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.source.network.media.model.category.NetworkCategory\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkAnimeProduction\nimport io.github.drumber.kitsune.data.source.network.media.model.relationship.NetworkMediaRelationship\nimport io.github.drumber.kitsune.data.source.network.media.model.streamer.NetworkStreamingLink\n\n@Type(\"anime\")\ndata class NetworkAnime(\n    @Id\n    override val id: String = \"\",\n    override val slug: String?,\n\n    override val description: String?,\n    override val titles: Titles?,\n    override val canonicalTitle: String?,\n    override val abbreviatedTitles: List<String>?,\n\n    override val averageRating: String?,\n    override val ratingFrequencies: NetworkRatingFrequencies?,\n    override val userCount: Int?,\n    override val favoritesCount: Int?,\n    override val popularityRank: Int?,\n    override val ratingRank: Int?,\n\n    override val startDate: String?,\n    override val endDate: String?,\n    override val nextRelease: String?,\n    override val tba: String?,\n    override val status: NetworkReleaseStatus?,\n\n    override val ageRating: AgeRating?,\n    override val ageRatingGuide: String?,\n    override val nsfw: Boolean?,\n\n    override val posterImage: Image?,\n    override val coverImage: Image?,\n\n    override val totalLength: Int?,\n    val episodeCount: Int?,\n    val episodeLength: Int?,\n    val youtubeVideoId: String?,\n    val subtype: NetworkAnimeSubtype?,\n\n    @Relationship(\"categories\")\n    override val categories: List<NetworkCategory>?,\n    @Relationship(\"animeProductions\")\n    val animeProduction: List<NetworkAnimeProduction>?,\n    @Relationship(\"streamingLinks\")\n    val streamingLinks: List<NetworkStreamingLink>?,\n    @Relationship(\"mediaRelationships\")\n    override val mediaRelationships: List<NetworkMediaRelationship>?\n) : NetworkMedia {\n\n    companion object {\n        fun empty(id: String) = NetworkAnime(\n            id = id,\n            slug = null,\n            description = null,\n            titles = null,\n            canonicalTitle = null,\n            abbreviatedTitles = null,\n            averageRating = null,\n            ratingFrequencies = null,\n            userCount = null,\n            favoritesCount = null,\n            popularityRank = null,\n            ratingRank = null,\n            startDate = null,\n            endDate = null,\n            nextRelease = null,\n            tba = null,\n            status = null,\n            ageRating = null,\n            ageRatingGuide = null,\n            nsfw = null,\n            posterImage = null,\n            coverImage = null,\n            totalLength = null,\n            episodeCount = null,\n            episodeLength = null,\n            youtubeVideoId = null,\n            subtype = null,\n            categories = null,\n            animeProduction = null,\n            streamingLinks = null,\n            mediaRelationships = null\n        )\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/NetworkAnimeSubtype.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkAnimeSubtype {\n    @JsonProperty(\"ONA\")\n    ONA,\n    @JsonProperty(\"OVA\")\n    OVA,\n    @JsonProperty(\"TV\")\n    TV,\n    @JsonProperty(\"movie\")\n    Movie,\n    @JsonProperty(\"music\")\n    Music,\n    @JsonProperty(\"special\")\n    Special\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/NetworkManga.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.source.network.media.model.category.NetworkCategory\nimport io.github.drumber.kitsune.data.source.network.media.model.relationship.NetworkMediaRelationship\n\n@Type(\"manga\")\ndata class NetworkManga(\n    @Id\n    override val id: String = \"\",\n    override val slug: String?,\n\n    override val description: String?,\n    override val titles: Titles?,\n    override val canonicalTitle: String?,\n    override val abbreviatedTitles: List<String>?,\n\n    override val averageRating: String?,\n    override val ratingFrequencies: NetworkRatingFrequencies?,\n    override val userCount: Int?,\n    override val favoritesCount: Int?,\n    override val popularityRank: Int?,\n    override val ratingRank: Int?,\n\n    override val startDate: String?,\n    override val endDate: String?,\n    override val nextRelease: String?,\n    override val tba: String?,\n    override val status: NetworkReleaseStatus?,\n\n    override val ageRating: AgeRating?,\n    override val ageRatingGuide: String?,\n    override val nsfw: Boolean?,\n\n    override val posterImage: Image?,\n    override val coverImage: Image?,\n\n    override val totalLength: Int?,\n    val chapterCount: Int?,\n    val volumeCount: Int?,\n    val subtype: NetworkMangaSubtype?,\n    val serialization: String?,\n\n    @Relationship(\"categories\")\n    override val categories: List<NetworkCategory>?,\n    @Relationship(\"mediaRelationships\")\n    override val mediaRelationships: List<NetworkMediaRelationship>?\n) : NetworkMedia {\n\n    companion object {\n        fun empty(id: String) = NetworkManga(\n            id = id,\n            slug = null,\n            description = null,\n            titles = null,\n            canonicalTitle = null,\n            abbreviatedTitles = null,\n            averageRating = null,\n            ratingFrequencies = null,\n            userCount = null,\n            favoritesCount = null,\n            popularityRank = null,\n            ratingRank = null,\n            startDate = null,\n            endDate = null,\n            nextRelease = null,\n            tba = null,\n            status = null,\n            ageRating = null,\n            ageRatingGuide = null,\n            nsfw = null,\n            posterImage = null,\n            coverImage = null,\n            totalLength = null,\n            chapterCount = null,\n            volumeCount = null,\n            subtype = null,\n            serialization = null,\n            categories = null,\n            mediaRelationships = null\n        )\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/NetworkMangaSubtype.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkMangaSubtype {\n    @JsonProperty(\"doujin\")\n    Doujin,\n    @JsonProperty(\"manga\")\n    Manga,\n    @JsonProperty(\"manhua\")\n    Manhua,\n    @JsonProperty(\"manhwa\")\n    Manhwa,\n    @JsonProperty(\"novel\")\n    Novel,\n    @JsonProperty(\"oel\")\n    Oel,\n    @JsonProperty(\"oneshot\")\n    Oneshot\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/NetworkMedia.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.source.network.media.model.category.NetworkCategory\nimport io.github.drumber.kitsune.data.source.network.media.model.relationship.NetworkMediaRelationship\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavoriteItem\n\nsealed interface NetworkMedia : NetworkFavoriteItem {\n    val id: String\n    val slug: String?\n\n    val description: String?\n    val titles: Titles?\n    val canonicalTitle: String?\n    val abbreviatedTitles: List<String>?\n\n    val averageRating: String?\n    val ratingFrequencies: NetworkRatingFrequencies?\n    val userCount: Int?\n    val favoritesCount: Int?\n    val popularityRank: Int?\n    val ratingRank: Int?\n\n    val startDate: String?\n    val endDate: String?\n    val nextRelease: String?\n    val tba: String?\n    val status: NetworkReleaseStatus?\n\n    val ageRating: AgeRating?\n    val ageRatingGuide: String?\n    val nsfw: Boolean?\n\n    val posterImage: Image?\n    val coverImage: Image?\n\n    val totalLength: Int?\n\n    // Relationships\n    val categories: List<NetworkCategory>?\n    val mediaRelationships: List<NetworkMediaRelationship>?\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/NetworkRatingFrequencies.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\ndata class NetworkRatingFrequencies(\n    @JsonProperty(\"2\") val r2: String?,\n    @JsonProperty(\"3\") val r3: String?,\n    @JsonProperty(\"4\") val r4: String?,\n    @JsonProperty(\"5\") val r5: String?,\n    @JsonProperty(\"6\") val r6: String?,\n    @JsonProperty(\"7\") val r7: String?,\n    @JsonProperty(\"8\") val r8: String?,\n    @JsonProperty(\"9\") val r9: String?,\n    @JsonProperty(\"10\") val r10: String?,\n    @JsonProperty(\"11\") val r11: String?,\n    @JsonProperty(\"12\") val r12: String?,\n    @JsonProperty(\"13\") val r13: String?,\n    @JsonProperty(\"14\") val r14: String?,\n    @JsonProperty(\"15\") val r15: String?,\n    @JsonProperty(\"16\") val r16: String?,\n    @JsonProperty(\"17\") val r17: String?,\n    @JsonProperty(\"18\") val r18: String?,\n    @JsonProperty(\"19\") val r19: String?,\n    @JsonProperty(\"20\") val r20: String?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/NetworkReleaseStatus.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkReleaseStatus {\n    @JsonProperty(\"current\")\n    Current,\n    @JsonProperty(\"finished\")\n    Finished,\n    @JsonProperty(\"tba\")\n    TBA,\n    @JsonProperty(\"unreleased\")\n    Unreleased,\n    @JsonProperty(\"upcoming\")\n    Upcoming\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/category/NetworkCategory.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.category\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"categories\")\ndata class NetworkCategory(\n    @Id\n    val id: String?,\n    val slug: String?,\n\n    val title: String?,\n    val description: String?,\n    val nsfw: Boolean?,\n\n    val totalMediaCount: Int?,\n    val childCount: Int?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/production/NetworkAnimeProduction.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.production\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"animeProductions\")\ndata class NetworkAnimeProduction(\n    @Id\n    val id: String?,\n    val role: NetworkAnimeProductionRole?,\n\n    @Relationship(\"producer\")\n    val producer: NetworkProducer?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/production/NetworkAnimeProductionRole.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.production\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkAnimeProductionRole {\n    @JsonProperty(\"licensor\")\n    Licensor,\n    @JsonProperty(\"producer\")\n    Producer,\n    @JsonProperty(\"studio\")\n    Studio\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/production/NetworkCasting.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.production\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\n\n@Type(\"castings\")\ndata class NetworkCasting(\n    @Id\n    val id: String?,\n    val role: String?,\n    val voiceActor: Boolean?,\n    val featured: Boolean?,\n    val language: String?,\n\n    @Relationship(\"character\")\n    val character: NetworkCharacter?,\n    @Relationship(\"person\")\n    val person: NetworkPerson?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/production/NetworkPerson.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.production\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.common.Image\n\n@Type(\"people\")\ndata class NetworkPerson(\n    @Id\n    val id: String?,\n    val name: String?,\n    val description: String?,\n    val image: Image?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/production/NetworkProducer.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.production\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"producers\")\ndata class NetworkProducer(\n    @Id\n    val id: String?,\n    val slug: String?,\n    val name: String?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/relationship/NetworkMediaRelationship.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.relationship\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkMedia\n\n@Type(\"mediaRelationships\")\ndata class NetworkMediaRelationship(\n    @Id\n    val id: String?,\n    val role: NetworkMediaRelationshipRole?,\n\n    @Relationship(\"destination\")\n    val media: NetworkMedia?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/relationship/NetworkMediaRelationshipRole.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.relationship\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\n/**\n * Media relationship roles, sort order from\n * https://github.com/hummingbird-me/kitsu-server/blob/the-future/app/models/media_relationship.rb\n */\nenum class NetworkMediaRelationshipRole {\n    @JsonProperty(\"sequel\")\n    Sequel,\n    @JsonProperty(\"prequel\")\n    Prequel,\n    @JsonProperty(\"alternative_setting\")\n    AlternativeSetting,\n    @JsonProperty(\"alternative_version\")\n    AlternativeVersion,\n    @JsonProperty(\"side_story\")\n    SideStory,\n    @JsonProperty(\"parent_story\")\n    ParentStory,\n    @JsonProperty(\"summary\")\n    Summary,\n    @JsonProperty(\"full_story\")\n    FullStory,\n    @JsonProperty(\"spinoff\")\n    Spinoff,\n    @JsonProperty(\"adaptation\")\n    Adaptation,\n    @JsonProperty(\"character\")\n    Character,\n    @JsonProperty(\"other\")\n    Other\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/streamer/NetworkStreamer.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.streamer\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"streamers\")\ndata class NetworkStreamer(\n    @Id\n    val id: String?,\n    val siteName: String?,\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/streamer/NetworkStreamingLink.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.streamer\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"streamingLinks\")\ndata class NetworkStreamingLink(\n    @Id\n    val id: String?,\n    val url: String?,\n    val subs: List<String>?,\n    val dubs: List<String>?,\n\n    @Relationship(\"streamer\")\n    val streamer: NetworkStreamer?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/unit/NetworkChapter.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.unit\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\n\n@Type(\"chapters\")\ndata class NetworkChapter(\n    @Id\n    override val id: String?,\n\n    override val description: String?,\n    override val titles: Titles?,\n    override val canonicalTitle: String?,\n\n    override val number: Int?,\n    val volumeNumber: Int?,\n    override val length: String?,\n\n    override val thumbnail: Image?,\n    val published: String?\n) : NetworkMediaUnit\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/unit/NetworkEpisode.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.unit\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\n\n@Type(\"episodes\")\ndata class NetworkEpisode(\n    @Id\n    override val id: String?,\n\n    override val description: String?,\n    override val titles: Titles?,\n    override val canonicalTitle: String?,\n\n    override val number: Int?,\n    val seasonNumber: Int?,\n    val relativeNumber: Int?,\n    override val length: String?,\n    val airdate: String?,\n\n    override val thumbnail: Image?\n) : NetworkMediaUnit\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/media/model/unit/NetworkMediaUnit.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.media.model.unit\n\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.Titles\n\nsealed interface NetworkMediaUnit {\n    val id: String?\n\n    val description: String?\n    val titles: Titles?\n    val canonicalTitle: String?\n\n    val number: Int?\n    val length: String?\n    val thumbnail: Image?\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/FavoriteNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.user.api.FavoriteApi\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavorite\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass FavoriteNetworkDataSource(\n    private val favoriteApi: FavoriteApi\n) {\n\n    suspend fun getAllFavorites(filter: Filter): List<NetworkFavorite>? {\n        return withContext(Dispatchers.IO) {\n            favoriteApi.getAllFavorites(filter.options).get()\n        }\n    }\n\n    suspend fun createFavorite(favorite: NetworkFavorite): NetworkFavorite? {\n        return withContext(Dispatchers.IO) {\n            favoriteApi.postFavorite(JSONAPIDocument(favorite)).get()\n        }\n    }\n\n    suspend fun deleteFavorite(id: String): Boolean {\n        return withContext(Dispatchers.IO) {\n            favoriteApi.deleteFavorite(id).isSuccessful\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/ProfileLinkNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user\n\nimport io.github.drumber.kitsune.data.source.network.user.api.ProfileLinkApi\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLinkSite\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass ProfileLinkNetworkDataSource(\n    private val profileLinkApi: ProfileLinkApi\n) {\n\n    suspend fun getAllProfileLinkSites(filter: Filter): List<NetworkProfileLinkSite>? {\n        return withContext(Dispatchers.IO) {\n            profileLinkApi.getAllProfileLinkSites(filter.options).get()\n        }\n    }\n\n    suspend fun createProfileLink(profileLink: NetworkProfileLink): NetworkProfileLink? {\n        return withContext(Dispatchers.IO) {\n            profileLinkApi.createProfileLink(profileLink).get()\n        }\n    }\n\n    suspend fun updateProfileLink(id: String, profileLink: NetworkProfileLink): NetworkProfileLink? {\n        return withContext(Dispatchers.IO) {\n            profileLinkApi.updateProfileLink(id, profileLink).get()\n        }\n    }\n\n    suspend fun deleteProfileLink(id: String): Boolean {\n        return withContext(Dispatchers.IO) {\n            profileLinkApi.deleteProfileLink(id).isSuccessful\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/UserNetworkDataSource.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.user.api.UserApi\nimport io.github.drumber.kitsune.data.source.network.user.api.UserImageUploadApi\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUserImageUpload\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.common.Filter\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass UserNetworkDataSource(\n    private val userApi: UserApi,\n    private val imageUploadApi: UserImageUploadApi\n) {\n\n    suspend fun getSelf(baseFilter: Filter): NetworkUser? {\n        val filter = baseFilter.copy()\n            .filter(\"self\", \"true\")\n\n        return withContext(Dispatchers.IO) {\n            userApi.getAllUsers(filter.options).get()?.firstOrNull()\n        }\n    }\n\n    suspend fun getUser(userId: String, filter: Filter): NetworkUser? {\n        return withContext(Dispatchers.IO) {\n            userApi.getUser(userId, filter.options).get()\n        }\n    }\n\n    suspend fun updateUser(userId: String, user: NetworkUser): NetworkUser? {\n        return withContext(Dispatchers.IO) {\n            userApi.updateUser(userId, JSONAPIDocument(user)).get()\n        }\n    }\n\n    suspend fun updateUserImage(userId: String, user: NetworkUserImageUpload): Boolean {\n        return withContext(Dispatchers.IO) {\n            imageUploadApi.updateUserImage(userId, JSONAPIDocument(user)).isSuccessful\n        }\n    }\n\n    suspend fun getProfileLinksForUser(userId: String, filter: Filter): List<NetworkProfileLink>? {\n        return withContext(Dispatchers.IO) {\n            userApi.getProfileLinksForUser(userId, filter.options).get()\n        }\n    }\n\n    suspend fun deleteWaifuRelationship(userId: String): Boolean {\n        return withContext(Dispatchers.IO) {\n            userApi.deleteWaifuRelationship(userId).isSuccessful\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/api/FavoriteApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavorite\nimport retrofit2.Response\nimport retrofit2.http.Body\nimport retrofit2.http.DELETE\nimport retrofit2.http.GET\nimport retrofit2.http.POST\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface FavoriteApi {\n\n    @GET(\"favorites\")\n    suspend fun getAllFavorites(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkFavorite>>\n\n    @POST(\"favorites\")\n    suspend fun postFavorite(\n        @Body favorite: JSONAPIDocument<NetworkFavorite>\n    ): JSONAPIDocument<NetworkFavorite>\n\n    @DELETE(\"favorites/{id}\")\n    suspend fun deleteFavorite(\n        @Path(\"id\") id: String\n    ): Response<Unit>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/api/ProfileLinkApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLinkSite\nimport retrofit2.Response\nimport retrofit2.http.Body\nimport retrofit2.http.DELETE\nimport retrofit2.http.GET\nimport retrofit2.http.PATCH\nimport retrofit2.http.POST\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface ProfileLinkApi {\n\n    @GET(\"profile-link-sites\")\n    suspend fun getAllProfileLinkSites(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkProfileLinkSite>>\n\n    @POST(\"profile-links\")\n    suspend fun createProfileLink(\n        @Body profileLink: NetworkProfileLink\n    ): JSONAPIDocument<NetworkProfileLink>\n\n    @PATCH(\"profile-links/{id}\")\n    suspend fun updateProfileLink(\n        @Path(\"id\") id: String,\n        @Body profileLink: NetworkProfileLink\n    ): JSONAPIDocument<NetworkProfileLink>\n\n    @DELETE(\"profile-links/{id}\")\n    suspend fun deleteProfileLink(\n        @Path(\"id\") id: String\n    ): Response<Unit>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/api/UserApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\nimport retrofit2.Response\nimport retrofit2.http.Body\nimport retrofit2.http.DELETE\nimport retrofit2.http.GET\nimport retrofit2.http.PATCH\nimport retrofit2.http.Path\nimport retrofit2.http.QueryMap\n\ninterface UserApi {\n\n    @GET(\"users\")\n    suspend fun getAllUsers(\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkUser>>\n\n    @GET(\"users/{id}\")\n    suspend fun getUser(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<NetworkUser>\n\n    @PATCH(\"users/{id}\")\n    suspend fun updateUser(\n        @Path(\"id\") id: String,\n        @Body user: JSONAPIDocument<NetworkUser>\n    ): JSONAPIDocument<NetworkUser>\n\n    @DELETE(\"users/{id}/relationships/waifu\")\n    suspend fun deleteWaifuRelationship(\n        @Path(\"id\") id: String\n    ): Response<Unit>\n\n    @GET(\"users/{id}/profile-links\")\n    suspend fun getProfileLinksForUser(\n        @Path(\"id\") id: String,\n        @QueryMap filter: Map<String, String> = emptyMap()\n    ): JSONAPIDocument<List<NetworkProfileLink>>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/api/UserImageUploadApi.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.api\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUserImageUpload\nimport retrofit2.Response\nimport retrofit2.http.Body\nimport retrofit2.http.PATCH\nimport retrofit2.http.Path\n\ninterface UserImageUploadApi {\n\n    @PATCH(\"users/{id}\")\n    suspend fun updateUserImage(\n        @Path(\"id\") id: String,\n        @Body user: JSONAPIDocument<NetworkUserImageUpload>\n    ): Response<Unit>\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/NetworkFavorite.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"favorites\")\ndata class NetworkFavorite(\n    @Id\n    val id: String? = null,\n    val favRank: Int? = null,\n\n    @Relationship(\"item\")\n    val item: NetworkFavoriteItem? = null,\n    @Relationship(\"user\")\n    val user: NetworkUser? = null\n)\n\ninterface NetworkFavoriteItem\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/NetworkRatingSystemPreference.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkRatingSystemPreference {\n    // 0.5, 1...10\n    @JsonProperty(\"advanced\")\n    Advanced,\n    // 0.5, 1...5\n    @JsonProperty(\"regular\")\n    Regular,\n    // :(, :|, :), :D\n    @JsonProperty(\"simple\")\n    Simple\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/NetworkSfwFilterPreference.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkSfwFilterPreference {\n    @JsonProperty(\"sfw\")\n    SFW,\n    @JsonProperty(\"nsfw_sometimes\")\n    NSFW_SOMETIMES,\n    @JsonProperty(\"nsfw_everywhere\")\n    NSFW_EVERYWHERE\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/NetworkTitleLanguagePreference.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkTitleLanguagePreference {\n    @JsonProperty(\"canonical\")\n    Canonical,\n    @JsonProperty(\"romanized\")\n    Romanized,\n    @JsonProperty(\"english\")\n    English\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/NetworkUser.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.common.Image\nimport io.github.drumber.kitsune.data.common.user.UserThemePreference\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.source.network.user.model.stats.NetworkUserStats\n\n@Type(\"users\")\ndata class NetworkUser(\n    @Id\n    val id: String?,\n    val createdAt: String? = null,\n    val updatedAt: String? = null,\n\n    val name: String? = null,\n    val slug: String? = null,\n    val email: String? = null,\n    val title: String? = null,\n\n    val avatar: Image? = null,\n    val coverImage: Image? = null,\n\n    val about: String? = null,\n    val location: String? = null,\n    val gender: String? = null,\n    val birthday: String? = null,\n    val waifuOrHusbando: String? = null,\n\n    val followersCount: Int? = null,\n    val followingCount: Int? = null,\n    val commentsCount: Int? = null,\n    val favoritesCount: Int? = null,\n    val likesGivenCount: Int? = null,\n    val reviewsCount: Int? = null,\n    val likesReceivedCount: Int? = null,\n    val postsCount: Int? = null,\n    val ratingsCount: Int? = null,\n    val mediaReactionsCount: Int? = null,\n\n    val country: String? = null,\n    val language: String? = null,\n    val timeZone: String? = null,\n    val theme: UserThemePreference? = null,\n\n    val sfwFilter: Boolean? = null,\n    val ratingSystem: NetworkRatingSystemPreference? = null,\n    val shareToGlobal: Boolean? = null,\n    val sfwFilterPreference: NetworkSfwFilterPreference? = null,\n    val titleLanguagePreference: NetworkTitleLanguagePreference? = null,\n\n    val profileCompleted: Boolean? = null,\n    val feedCompleted: Boolean? = null,\n    val proTier: String? = null,\n    val proExpiresAt: String? = null,\n    val aoPro: String? = null,\n\n    val facebookId: String? = null,\n    val confirmed: Boolean? = null,\n    val status: String? = null,\n    val hasPassword: Boolean? = null,\n    val subscribedToNewsletter: Boolean? = null,\n\n    @Relationship(\"stats\")\n    val stats: List<NetworkUserStats>? = null,\n    @Relationship(\"favorites\")\n    val favorites: List<NetworkFavorite>? = null,\n    @Relationship(\"waifu\")\n    val waifu: NetworkCharacter? = null,\n    @Relationship(\"profileLinks\")\n    val profileLinks: List<NetworkProfileLink>? = null,\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/NetworkUserImageUpload.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"users\")\ndata class NetworkUserImageUpload(\n    @Id\n    val id: String?,\n    /** Avatar image as Base64 encoded string */\n    val avatar: String? = null,\n    /** Cover image as Base64 encoded string */\n    val coverImage: String? = null\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/profilelinks/NetworkProfileLink.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model.profilelinks\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Relationship\nimport com.github.jasminb.jsonapi.annotations.Type\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\n\n@Type(\"profileLinks\")\ndata class NetworkProfileLink(\n    @Id\n    val id: String?,\n    val url: String?,\n\n    @Relationship(\"profileLinkSite\")\n    val profileLinkSite: NetworkProfileLinkSite?,\n    @Relationship(\"user\")\n    val user: NetworkUser?\n) {\n    companion object {\n        fun empty() = NetworkProfileLink(null, null, null, null)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/profilelinks/NetworkProfileLinkSite.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model.profilelinks\n\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"profileLinkSites\")\ndata class NetworkProfileLinkSite(\n    @Id\n    val id: String?,\n    val name: String?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/stats/NetworkAmountConsumedPercentiles.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model.stats\n\ndata class NetworkAmountConsumedPercentiles(\n    val media: Float?,\n    val units: Float?,\n    val time: Float?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/stats/NetworkUserStats.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model.stats\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo\nimport com.github.jasminb.jsonapi.annotations.Id\nimport com.github.jasminb.jsonapi.annotations.Type\n\n@Type(\"stats\")\ndata class NetworkUserStats(\n    @Id\n    val id: String?,\n    val kind: NetworkUserStatsKind?,\n\n    @JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME,\n        include = JsonTypeInfo.As.EXTERNAL_PROPERTY,\n        property = \"kind\",\n        visible = true\n    )\n    val statsData: NetworkUserStatsData?\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/stats/NetworkUserStatsData.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model.stats\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes\n\n@JsonSubTypes(\n    JsonSubTypes.Type(\n        value = NetworkUserStatsData.NetworkCategoryBreakdownData::class,\n        names = [\"anime-category-breakdown\", \"manga-category-breakdown\"]\n    ),\n    JsonSubTypes.Type(\n        value = NetworkUserStatsData.NetworkAmountConsumedData::class,\n        names = [\"anime-amount-consumed\", \"manga-amount-consumed\"]\n    )\n)\nsealed class NetworkUserStatsData {\n    data class NetworkCategoryBreakdownData(\n        val total: Int?,\n        val categories: Map<String, Int>?\n    ) : NetworkUserStatsData()\n\n    data class NetworkAmountConsumedData(\n        val time: Long?,\n        val media: Int?,\n        val units: Int?,\n        val completed: Int?,\n        val percentiles: NetworkAmountConsumedPercentiles?,\n        val averageDiffs: NetworkAmountConsumedPercentiles?\n    ) : NetworkUserStatsData()\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/source/network/user/model/stats/NetworkUserStatsKind.kt",
    "content": "package io.github.drumber.kitsune.data.source.network.user.model.stats\n\nimport com.fasterxml.jackson.annotation.JsonProperty\n\nenum class NetworkUserStatsKind {\n    @JsonProperty(\"anime-activity-history\")\n    AnimeActivityHistory,\n    @JsonProperty(\"anime-amount-consumed\")\n    AnimeAmountConsumed,\n    @JsonProperty(\"anime-category-breakdown\")\n    AnimeCategoryBreakdown,\n    @JsonProperty(\"anime-favorite-year\")\n    AnimeFavoriteYear,\n\n    @JsonProperty(\"manga-activity-history\")\n    MangaActivityHistory,\n    @JsonProperty(\"manga-amount-consumed\")\n    MangaAmountConsumed,\n    @JsonProperty(\"manga-category-breakdown\")\n    MangaCategoryBreakdown,\n    @JsonProperty(\"manga-favorite-year\")\n    MangaFavoriteYear\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/data/utils/InvalidatingPagingSourceFactory.kt",
    "content": "package io.github.drumber.kitsune.data.utils\n\nimport androidx.paging.PagingSource\nimport java.util.concurrent.locks.ReentrantLock\nimport kotlin.concurrent.withLock\n\n/**\n * Modified version of [androidx.paging.InvalidatingPagingSourceFactory] accepting a custom input property.\n */\nclass InvalidatingPagingSourceFactory<Key : Any, Value : Any, Input: Any>(\n    private val pagingSourceFactory: (input: Input) -> PagingSource<Key, Value>\n) {\n    private val lock = ReentrantLock()\n\n    private var pagingSources: List<PagingSource<Key, Value>> = emptyList()\n\n    fun createPagingSource(input: Input): PagingSource<Key, Value> {\n        return pagingSourceFactory(input).also {\n            lock.withLock {\n                pagingSources = pagingSources + it\n            }\n        }\n    }\n\n    fun invalidate() {\n        val previousList = lock.withLock {\n            pagingSources.also {\n                pagingSources = emptyList()\n            }\n        }\n\n        for (pagingSource in previousList) {\n            if (!pagingSource.invalid) {\n                pagingSource.invalidate()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/di/AppModule.kt",
    "content": "package io.github.drumber.kitsune.di\n\nval appModule = listOf(\n    networkModule,\n    viewModelModule,\n    dataModule,\n    databaseModule,\n    domainModule\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/di/DataModule.kt",
    "content": "package io.github.drumber.kitsune.di\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport io.github.drumber.kitsune.constants.GitHub\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.AlgoliaKeyRepository\nimport io.github.drumber.kitsune.data.repository.AnimeRepository\nimport io.github.drumber.kitsune.data.repository.AppUpdateRepository\nimport io.github.drumber.kitsune.data.repository.CastingRepository\nimport io.github.drumber.kitsune.data.repository.CategoryRepository\nimport io.github.drumber.kitsune.data.repository.CharacterRepository\nimport io.github.drumber.kitsune.data.repository.FavoriteRepository\nimport io.github.drumber.kitsune.data.repository.LibraryChangeListener\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.data.repository.MangaRepository\nimport io.github.drumber.kitsune.data.repository.MappingRepository\nimport io.github.drumber.kitsune.data.repository.MediaUnitRepository\nimport io.github.drumber.kitsune.data.repository.ProfileLinkRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.data.repository.WidgetLibraryChangeListener\nimport io.github.drumber.kitsune.data.source.local.auth.AccessTokenLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.auth.AccessTokenPreference\nimport io.github.drumber.kitsune.data.source.local.library.LibraryLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.user.UserLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.user.UserPreferences\nimport io.github.drumber.kitsune.data.source.network.algolia.AlgoliaKeyNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.algolia.api.AlgoliaKeyApi\nimport io.github.drumber.kitsune.data.source.network.appupdate.AppReleaseNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.appupdate.api.GitHubApi\nimport io.github.drumber.kitsune.data.source.network.auth.AccessTokenNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.auth.api.AuthenticationApi\nimport io.github.drumber.kitsune.data.source.network.character.CharacterNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.character.api.CharacterApi\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkMediaCharacter\nimport io.github.drumber.kitsune.data.source.network.library.LibraryNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.library.api.LibraryEntryApi\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryEntry\nimport io.github.drumber.kitsune.data.source.network.mapping.MappingNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.mapping.api.MappingApi\nimport io.github.drumber.kitsune.data.source.network.mapping.model.NetworkMapping\nimport io.github.drumber.kitsune.data.source.network.media.AnimeNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.CastingNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.CategoryNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.ChapterNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.EpisodeNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.MangaNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.media.api.AnimeApi\nimport io.github.drumber.kitsune.data.source.network.media.api.CastingApi\nimport io.github.drumber.kitsune.data.source.network.media.api.CategoryApi\nimport io.github.drumber.kitsune.data.source.network.media.api.ChapterApi\nimport io.github.drumber.kitsune.data.source.network.media.api.EpisodeApi\nimport io.github.drumber.kitsune.data.source.network.media.api.MangaApi\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.source.network.media.model.category.NetworkCategory\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkAnimeProduction\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkCasting\nimport io.github.drumber.kitsune.data.source.network.media.model.production.NetworkProducer\nimport io.github.drumber.kitsune.data.source.network.media.model.relationship.NetworkMediaRelationship\nimport io.github.drumber.kitsune.data.source.network.media.model.streamer.NetworkStreamer\nimport io.github.drumber.kitsune.data.source.network.media.model.streamer.NetworkStreamingLink\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkChapter\nimport io.github.drumber.kitsune.data.source.network.media.model.unit.NetworkEpisode\nimport io.github.drumber.kitsune.data.source.network.user.FavoriteNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.user.ProfileLinkNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.user.UserNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.user.api.FavoriteApi\nimport io.github.drumber.kitsune.data.source.network.user.api.ProfileLinkApi\nimport io.github.drumber.kitsune.data.source.network.user.api.UserApi\nimport io.github.drumber.kitsune.data.source.network.user.api.UserImageUploadApi\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavorite\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUserImageUpload\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLinkSite\nimport io.github.drumber.kitsune.data.source.network.user.model.stats.NetworkUserStats\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport org.koin.android.ext.koin.androidApplication\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.core.qualifier.named\nimport org.koin.dsl.module\n\nval dataModule = module {\n    // Auth\n    factory { createAuthService(get()) }\n    single { AccessTokenNetworkDataSource(get()) }\n    single<AccessTokenLocalDataSource> { AccessTokenPreference(androidContext(), get()) }\n    single { AccessTokenRepository(get(), get()) }\n\n    // User\n    factory {\n        createService<UserApi>(\n            get(),\n            get(),\n            NetworkUser::class.java,\n            NetworkUserStats::class.java,\n            NetworkFavorite::class.java,\n            NetworkAnime::class.java,\n            NetworkManga::class.java,\n            NetworkCharacter::class.java\n        )\n    }\n    factory {\n        createService<UserImageUploadApi>(\n            get(),\n            get(),\n            NetworkUserImageUpload::class.java\n        )\n    }\n    single { UserNetworkDataSource(get(), get()) }\n    single<UserLocalDataSource> { UserPreferences(androidContext(), get()) }\n    single { UserRepository(get(), get(), CoroutineScope(SupervisorJob() + Dispatchers.Default)) }\n\n    // Algolia\n    factory { createService<AlgoliaKeyApi>(get(), get()) }\n    single { AlgoliaKeyNetworkDataSource(get()) }\n    single { AlgoliaKeyRepository(get()) }\n\n    // ProfileLinks\n    factory {\n        createService<ProfileLinkApi>(\n            get(),\n            get(),\n            NetworkProfileLink::class.java,\n            NetworkProfileLinkSite::class.java,\n            NetworkUser::class.java\n        )\n    }\n    single { ProfileLinkNetworkDataSource(get()) }\n    single { ProfileLinkRepository(get()) }\n\n    // Favorite\n    factory {\n        createService<FavoriteApi>(\n            get(),\n            get(),\n            NetworkFavorite::class.java,\n            NetworkAnime::class.java,\n            NetworkManga::class.java,\n            NetworkUser::class.java\n        )\n    }\n    single { FavoriteNetworkDataSource(get()) }\n    single { FavoriteRepository(get()) }\n\n    // Anime\n    factory {\n        createService<AnimeApi>(\n            get(), get(),\n            NetworkAnime::class.java,\n            NetworkManga::class.java,\n            NetworkCategory::class.java,\n            NetworkAnimeProduction::class.java,\n            NetworkProducer::class.java,\n            NetworkStreamingLink::class.java,\n            NetworkStreamer::class.java,\n            NetworkMediaRelationship::class.java\n        )\n    }\n    single { AnimeNetworkDataSource(get()) }\n    single { AnimeRepository(get()) }\n\n    // Manga\n    factory {\n        createService<MangaApi>(\n            get(), get(),\n            NetworkManga::class.java,\n            NetworkAnime::class.java,\n            NetworkCategory::class.java,\n            NetworkMediaRelationship::class.java\n        )\n    }\n    single { MangaNetworkDataSource(get()) }\n    single { MangaRepository(get()) }\n\n    // Media Unit\n    factory { createService<EpisodeApi>(get(), get(), NetworkEpisode::class.java) }\n    factory { createService<ChapterApi>(get(), get(), NetworkChapter::class.java) }\n    single { EpisodeNetworkDataSource(get()) }\n    single { ChapterNetworkDataSource(get()) }\n    single { MediaUnitRepository(get(), get()) }\n\n    // Casting\n    factory {\n        createService<CastingApi>(\n            get(),\n            get(),\n            NetworkCasting::class.java,\n            NetworkCharacter::class.java\n        )\n    }\n    single { CastingNetworkDataSource(get()) }\n    single { CastingRepository(get()) }\n\n    // Category\n    factory { createService<CategoryApi>(get(), get(), NetworkCategory::class.java) }\n    single { CategoryNetworkDataSource(get()) }\n    single { CategoryRepository(get()) }\n\n    // Character\n    factory {\n        createService<CharacterApi>(\n            get(),\n            get(),\n            NetworkCharacter::class.java,\n            NetworkMediaCharacter::class.java,\n            NetworkAnime::class.java,\n            NetworkManga::class.java\n        )\n    }\n    single { CharacterNetworkDataSource(get()) }\n    single { CharacterRepository(get()) }\n\n    // Mapping\n    factory {\n        createService<MappingApi>(\n            get(),\n            get(),\n            NetworkMapping::class.java\n        )\n    }\n    single { MappingNetworkDataSource(get()) }\n    single { MappingRepository(get()) }\n\n    // Library Entry\n    factory {\n        createService<LibraryEntryApi>(\n            get(), get(),\n            NetworkLibraryEntry::class.java,\n            NetworkAnime::class.java,\n            NetworkManga::class.java\n        )\n    }\n    single { LibraryNetworkDataSource(get()) }\n    single { LibraryLocalDataSource(get()) }\n    single<LibraryChangeListener> { WidgetLibraryChangeListener(androidApplication(), get()) }\n    single {\n        LibraryRepository(\n            get(),\n            get(),\n            get(),\n            CoroutineScope(SupervisorJob() + Dispatchers.Default)\n        )\n    }\n\n    // App Update\n    factory {\n        createService<GitHubApi>(\n            get(named(\"unauthenticated\")),\n            get(),\n            GitHub.API_URL\n        )\n    }\n    single { AppReleaseNetworkDataSource(get()) }\n    single { AppUpdateRepository(get()) }\n}\n\nprivate fun createAuthService(objectMapper: ObjectMapper) = createService<AuthenticationApi>(\n    createHttpClientBuilder(addLoggingInterceptor = false).build(),\n    objectMapper,\n    Kitsu.OAUTH_URL\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/di/DatabaseModule.kt",
    "content": "package io.github.drumber.kitsune.di\n\nimport io.github.drumber.kitsune.data.source.local.LocalDatabase\nimport org.koin.android.ext.koin.androidApplication\nimport org.koin.dsl.module\n\nval databaseModule = module {\n    single { LocalDatabase.createLocalDatabase(androidApplication()) }\n    factory { get<LocalDatabase>().libraryEntryDao() }\n    factory { get<LocalDatabase>().libraryEntryModificationDao() }\n    factory { get<LocalDatabase>().libraryEntryWithModificationDao() }\n    factory { get<LocalDatabase>().remoteKeyDao() }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/di/DomainModule.kt",
    "content": "package io.github.drumber.kitsune.di\n\nimport io.github.drumber.kitsune.domain.auth.IsUserLoggedInUseCase\nimport io.github.drumber.kitsune.domain.auth.LogInUserUseCase\nimport io.github.drumber.kitsune.domain.auth.LogOutUserUseCase\nimport io.github.drumber.kitsune.domain.auth.RefreshAccessTokenIfExpiredUseCase\nimport io.github.drumber.kitsune.domain.auth.RefreshAccessTokenUseCase\nimport io.github.drumber.kitsune.domain.library.FetchLibraryEntriesForWidgetUseCase\nimport io.github.drumber.kitsune.domain.library.GetLibraryEntriesWithModificationsPagerUseCase\nimport io.github.drumber.kitsune.domain.library.SearchLibraryEntriesWithLocalModificationsPagerUseCase\nimport io.github.drumber.kitsune.domain.library.SynchronizeLocalLibraryModificationsUseCase\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryProgressUseCase\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryRatingUseCase\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryUseCase\nimport io.github.drumber.kitsune.domain.user.GetLocalUserIdUseCase\nimport io.github.drumber.kitsune.domain.user.UpdateLocalUserUseCase\nimport io.github.drumber.kitsune.domain.work.UpdateLibraryWidgetUseCase\nimport org.koin.dsl.module\n\nval domainModule = module {\n    // Auth\n    factory { IsUserLoggedInUseCase(get()) }\n    factory { LogInUserUseCase(get(), get(), get()) }\n    factory { LogOutUserUseCase(get(), get()) }\n    factory { RefreshAccessTokenIfExpiredUseCase(get(), get()) }\n    factory { RefreshAccessTokenUseCase(get(), get(), get()) }\n\n    // User\n    factory { GetLocalUserIdUseCase(get()) }\n    factory { UpdateLocalUserUseCase(get(), get(), get()) }\n\n    // Library\n    factory { GetLibraryEntriesWithModificationsPagerUseCase(get()) }\n    factory { SearchLibraryEntriesWithLocalModificationsPagerUseCase(get()) }\n    factory { SynchronizeLocalLibraryModificationsUseCase(get(), get()) }\n    factory { UpdateLibraryEntryProgressUseCase(get()) }\n    factory { UpdateLibraryEntryRatingUseCase(get()) }\n    factory { UpdateLibraryEntryUseCase(get()) }\n    factory { FetchLibraryEntriesForWidgetUseCase(get(), get()) }\n\n    // Work\n    factory { UpdateLibraryWidgetUseCase() }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/di/NetworkModule.kt",
    "content": "package io.github.drumber.kitsune.di\n\nimport android.content.Context\nimport android.os.Parcelable\nimport com.algolia.search.model.filter.Filter\nimport com.fasterxml.jackson.annotation.JsonInclude\nimport com.fasterxml.jackson.databind.DeserializationFeature\nimport com.fasterxml.jackson.databind.MapperFeature\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.databind.module.SimpleModule\nimport com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder\nimport com.github.jasminb.jsonapi.ResourceConverter\nimport com.github.jasminb.jsonapi.retrofit.JSONAPIConverterFactory\nimport io.github.drumber.kitsune.BuildConfig\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.util.json.AlgoliaFacetValueDeserializer\nimport io.github.drumber.kitsune.util.json.AlgoliaNumericValueDeserializer\nimport io.github.drumber.kitsune.util.json.IgnoreParcelablePropertyMixin\nimport io.github.drumber.kitsune.util.network.AuthenticationInterceptor\nimport io.github.drumber.kitsune.util.network.AuthenticationInterceptorImpl\nimport io.github.drumber.kitsune.util.network.UserAgentInterceptor\nimport okhttp3.Cache\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.logging.HttpLoggingInterceptor\nimport org.koin.core.qualifier.named\nimport org.koin.dsl.module\nimport retrofit2.Retrofit\nimport retrofit2.converter.jackson.JacksonConverterFactory\nimport java.io.File\nimport java.util.concurrent.TimeUnit\n\nval networkModule = module {\n    single { createHttpClient(get(), get()) }\n    single(named(\"unauthenticated\")) { createHttpClientBuilder().build() }\n    single(named(\"images\")) { createHttpClientBuilder(false).build() }\n    single { createObjectMapper() }\n    factory<AuthenticationInterceptor> { AuthenticationInterceptorImpl(get()) }\n}\n\nfun createHttpClientBuilder(addLoggingInterceptor: Boolean = true) = OkHttpClient.Builder()\n    .addInterceptor(createUserAgentInterceptor())\n    .apply {\n        if (addLoggingInterceptor) {\n            addNetworkInterceptor(createHttpLoggingInterceptor())\n        }\n    }\n    .connectTimeout(30, TimeUnit.SECONDS)\n    .readTimeout(60, TimeUnit.SECONDS)\n    .writeTimeout(60, TimeUnit.SECONDS)\n\nprivate fun createHttpClient(context: Context, authenticationInterceptor: AuthenticationInterceptor) =\n    createHttpClientBuilder()\n        .addInterceptor(authenticationInterceptor)\n        .authenticator(authenticationInterceptor)\n        .cache(Cache(\n            directory = File(context.cacheDir, \"http_cache\"),\n            maxSize = 1024L * 1024L * 5L // 5 MiB\n        ))\n        .build()\n\nprivate fun createHttpLoggingInterceptor() = HttpLoggingInterceptor().apply {\n    level = when (BuildConfig.DEBUG) {\n        true -> HttpLoggingInterceptor.Level.HEADERS\n        false -> HttpLoggingInterceptor.Level.BASIC\n    }\n    redactHeader(\"Authorization\")\n}\n\nfun createUserAgentInterceptor() =\n    UserAgentInterceptor(\"Kitsune/${BuildConfig.VERSION_NAME}\")\n\nfun createObjectMapper(): ObjectMapper = jacksonMapperBuilder()\n    .serializationInclusion(JsonInclude.Include.NON_NULL)\n    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n    .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true)\n    .configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false)\n    .addMixIn(Parcelable::class.java, IgnoreParcelablePropertyMixin::class.java)\n    .addModule(\n        SimpleModule().addDeserializer(\n            Filter.Facet.Value::class.java,\n            AlgoliaFacetValueDeserializer()\n        )\n    )\n    .addModule(\n        SimpleModule().addDeserializer(\n            Filter.Numeric.Value::class.java,\n            AlgoliaNumericValueDeserializer()\n        )\n    )\n    .build()\n\nfun createConverterFactory(\n    httpClient: OkHttpClient,\n    objectMapper: ObjectMapper,\n    vararg classes: Class<*>\n): JSONAPIConverterFactory {\n    val resourceConverter = ResourceConverter(objectMapper, *classes)\n    resourceConverter.setGlobalResolver { url ->\n        val request = httpClient.newCall(Request.Builder().url(url).build())\n        request.execute().body?.bytes()\n    }\n    return JSONAPIConverterFactory(resourceConverter)\n}\n\ninline fun <reified T> createService(\n    httpClient: OkHttpClient,\n    objectMapper: ObjectMapper,\n    vararg classes: Class<*>,\n    baseUrl: String = Kitsu.API_URL\n): T {\n    return Retrofit.Builder()\n        .baseUrl(baseUrl)\n        .client(httpClient)\n        .addConverterFactory(createConverterFactory(httpClient, objectMapper, *classes))\n        .addConverterFactory(JacksonConverterFactory.create(objectMapper))\n        .build()\n        .create(T::class.java)\n}\n\ninline fun <reified T> createService(\n    httpClient: OkHttpClient,\n    objectMapper: ObjectMapper,\n    baseUrl: String = Kitsu.API_URL\n): T {\n    return Retrofit.Builder()\n        .baseUrl(baseUrl)\n        .client(httpClient)\n        .addConverterFactory(JacksonConverterFactory.create(objectMapper))\n        .build()\n        .create(T::class.java)\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/di/ViewModelModule.kt",
    "content": "package io.github.drumber.kitsune.di\n\nimport io.github.drumber.kitsune.ui.authentication.LoginViewModel\nimport io.github.drumber.kitsune.ui.details.DetailsViewModel\nimport io.github.drumber.kitsune.ui.details.characters.CharacterDetailsViewModel\nimport io.github.drumber.kitsune.ui.details.characters.CharactersViewModel\nimport io.github.drumber.kitsune.ui.details.episodes.EpisodesViewModel\nimport io.github.drumber.kitsune.ui.library.LibraryViewModel\nimport io.github.drumber.kitsune.ui.library.editentry.LibraryEditEntryViewModel\nimport io.github.drumber.kitsune.ui.main.MainActivityViewModel\nimport io.github.drumber.kitsune.ui.main.MainFragmentViewModel\nimport io.github.drumber.kitsune.ui.medialist.MediaListViewModel\nimport io.github.drumber.kitsune.ui.onboarding.OnboardingViewModel\nimport io.github.drumber.kitsune.ui.profile.ProfileViewModel\nimport io.github.drumber.kitsune.ui.profile.editprofile.EditProfileViewModel\nimport io.github.drumber.kitsune.ui.search.SearchViewModel\nimport io.github.drumber.kitsune.ui.search.categories.CategoriesViewModel\nimport io.github.drumber.kitsune.ui.settings.AppLogsViewModel\nimport io.github.drumber.kitsune.ui.settings.SettingsViewModel\nimport org.koin.core.module.dsl.viewModel\nimport org.koin.dsl.module\n\nval viewModelModule = module {\n    viewModel { OnboardingViewModel(get(), get(), get()) }\n    viewModel { MainActivityViewModel(get(), get()) }\n    viewModel { MainFragmentViewModel(get(), get()) }\n    viewModel { SearchViewModel(get()) }\n    viewModel { MediaListViewModel(get(), get()) }\n    viewModel { CategoriesViewModel(get()) }\n    viewModel { LibraryViewModel(get(), get(), get(), get(), get(), get(), get(), get()) }\n    viewModel { LibraryEditEntryViewModel(get(), get()) }\n    viewModel { LoginViewModel(get()) }\n    viewModel { ProfileViewModel(get(), get()) }\n    viewModel { EditProfileViewModel(get(), get(), get()) }\n    viewModel { DetailsViewModel(get(), get(), get(), get(), get(), get(), get(), get()) }\n    viewModel { EpisodesViewModel(get(), get(), get(), get()) }\n    viewModel { CharactersViewModel(get(), get()) }\n    viewModel { CharacterDetailsViewModel(get(), get(), get()) }\n    viewModel { SettingsViewModel(get(), get()) }\n    viewModel { AppLogsViewModel() }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/algolia/FilterCollection.kt",
    "content": "package io.github.drumber.kitsune.domain.algolia\n\nimport com.algolia.instantsearch.filter.state.FilterGroupID\nimport com.algolia.instantsearch.filter.state.Filters\nimport com.algolia.search.model.filter.Filter\n\ndata class FilterCollection(\n    val facetGroups: List<FilterCollectionEntry<Filter.Facet>> = emptyList(),\n    val tagGroups: List<FilterCollectionEntry<Filter.Tag>> = emptyList(),\n    val numericGroups: List<FilterCollectionEntry<Filter.Numeric>> = emptyList()\n)\n\ndata class FilterCollectionEntry<T : Filter>(\n    val filterGroupID: FilterGroupID,\n    val filters: Set<T>\n)\n\nfun Filters.toFilterCollection() = FilterCollection(\n    getFacetGroups().toEntryList(),\n    getTagGroups().toEntryList(),\n    getNumericGroups().toEntryList()\n)\n\nfun FilterCollection.toCombinedMap(): Map<FilterGroupID, Set<Filter>> {\n    return facetGroups.toMap() + tagGroups.toMap() + numericGroups.toMap()\n}\n\nprivate fun <T : Filter> Map<FilterGroupID, Set<T>>.toEntryList(): List<FilterCollectionEntry<T>> {\n    return this.map {\n        FilterCollectionEntry(it.key, it.value)\n    }\n}\n\nprivate fun <T : Filter> List<FilterCollectionEntry<T>>.toMap(): Map<FilterGroupID, Set<T>> {\n    return buildMap {\n        this@toMap.forEach { entry ->\n            put(entry.filterGroupID, entry.filters)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/algolia/SearchProvider.kt",
    "content": "package io.github.drumber.kitsune.domain.algolia\n\nimport com.algolia.instantsearch.searcher.hits.HitsSearcher\nimport com.algolia.instantsearch.searcher.hits.SearchForQuery\nimport com.algolia.search.client.ClientSearch\nimport com.algolia.search.logging.LogLevel\nimport com.algolia.search.model.APIKey\nimport com.algolia.search.model.ApplicationID\nimport com.algolia.search.model.IndexName\nimport com.algolia.search.model.search.Query\nimport io.github.drumber.kitsune.BuildConfig\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.common.exception.InvalidDataException\nimport io.github.drumber.kitsune.data.common.exception.SearchProviderUnavailableException\nimport io.github.drumber.kitsune.data.presentation.model.algolia.SearchType\nimport io.github.drumber.kitsune.data.repository.AlgoliaKeyRepository\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.withContext\n\nclass SearchProvider(\n    private val algoliaKeyRepository: AlgoliaKeyRepository\n) {\n\n    private var clientSearch: ClientSearch? = null\n    private var searcherIndex: HitsSearcher? = null\n\n    var isInitialized = false\n        private set\n\n    suspend fun createSearchClient(\n        searchType: SearchType,\n        query: Query,\n        triggerSearchFor: SearchForQuery = SearchForQuery.All,\n        createdListener: suspend (HitsSearcher) -> Unit\n    ) {\n        searcherIndex?.cancel() // cancel any previous created searcher\n        isInitialized = false\n\n        val algoliaKeys = getAlgoliaKeysAsync().await()\n            ?: throw SearchProviderUnavailableException()\n        val algoliaKey = searchType.getAlgoliaKey(algoliaKeys)\n        val apiKey = algoliaKey?.key ?: throw InvalidDataException(\"Algolia API Key is null.\")\n        val apiIndex = algoliaKey.index ?: throw InvalidDataException(\"Algolia index is null.\")\n\n        val logLevel = if (BuildConfig.DEBUG) LogLevel.Headers else LogLevel.Info\n        val client = ClientSearch(ApplicationID(Kitsu.ALGOLIA_APP_ID), APIKey(apiKey), logLevel)\n\n        val searcher = HitsSearcher(\n            client, IndexName(apiIndex), query,\n            triggerSearchFor = triggerSearchFor\n        )\n        clientSearch = client\n        searcherIndex = searcher\n\n        isInitialized = true\n        withContext(Dispatchers.Main) {\n            createdListener(searcher)\n        }\n    }\n\n    private suspend fun getAlgoliaKeysAsync() = withContext(Dispatchers.IO) {\n        async {\n            try {\n                algoliaKeyRepository.getAllAlgoliaKeys()\n            } catch (e: Exception) {\n                logE(\"Failed to obtain algolia search keys.\", e)\n                null\n            }\n        }\n    }\n\n    fun cancel() {\n        searcherIndex?.cancel()\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/auth/IsUserLoggedInUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\n\nclass IsUserLoggedInUseCase(\n    private val accessTokenRepository: AccessTokenRepository\n) {\n\n    operator fun invoke() = accessTokenRepository.hasAccessToken()\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/auth/LogInUserUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.logI\nimport retrofit2.HttpException\n\nclass LogInUserUseCase(\n    private val userRepository: UserRepository,\n    private val accessTokenRepository: AccessTokenRepository,\n    private val isUserLoggedIn: IsUserLoggedInUseCase\n) {\n\n    suspend operator fun invoke(username: String, password: String): LoginResult {\n        if (isUserLoggedIn()) {\n            logI(\"Login: Did not log in because the user is already logged in.\")\n            return LoginResult.AlreadyLoggedIn\n        }\n\n        logI(\"Login: Obtaining access token.\")\n        val accessToken = try {\n            accessTokenRepository.obtainAccessToken(username, password)\n        } catch (e: HttpException) {\n            logE(\"Login: Failed to obtain access token.\", e)\n            return when (e.code()) {\n                400 -> LoginResult.Failure\n                else -> LoginResult.Error(e)\n            }\n        } catch (e: Exception) {\n            logE(\"Login: Failed to obtain access token.\", e)\n            return LoginResult.Error(e)\n        }\n\n        val localUser = try {\n            userRepository.fetchAndStoreLocalUserFromNetwork()\n            userRepository.localUser.value\n        } catch (e: Exception) {\n            logE(\"Login: Failed to update local user from network.\", e)\n            null\n        }\n\n        logI(\"Login: Successfully logged in.\")\n        return LoginResult.Success(accessToken, localUser)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/auth/LogOutUserUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.util.logI\n\nclass LogOutUserUseCase(\n    private val userRepository: UserRepository,\n    private val accessTokenRepository: AccessTokenRepository\n) {\n\n    suspend operator fun invoke() {\n        logI(\"Logout: Clearing access token and local user.\")\n        accessTokenRepository.clearAccessToken()\n        userRepository.clearLocalUser()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/auth/LoginResult.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\n\nsealed interface LoginResult {\n    /** Successfully logged in */\n    data class Success(val accessToken: LocalAccessToken, val localUser: LocalUser?) : LoginResult\n\n    /** Login failed, e.g. wrong credentials. */\n    data object Failure : LoginResult\n\n    /** User is already logged in */\n    data object AlreadyLoggedIn : LoginResult\n\n    /** Login failed due to an error. */\n    data class Error(val exception: Exception) : LoginResult\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/auth/RefreshAccessTokenIfExpiredUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.util.logI\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.seconds\n\nclass RefreshAccessTokenIfExpiredUseCase(\n    private val accessTokenRepository: AccessTokenRepository,\n    private val refreshAccessToken: RefreshAccessTokenUseCase\n) {\n\n    companion object {\n        val REFRESH_PRIOR_TIME = 1.days // refresh access token one day before it expires\n    }\n\n    suspend operator fun invoke(): RefreshResult? {\n        val accessToken = accessTokenRepository.getAccessToken() ?: return null\n\n        if (accessToken.isAccessTokenConsideredExpired()) {\n            logI(\"Refresh: Access token is considered expired.\")\n            return refreshAccessToken()\n        }\n        return null\n    }\n\n    /**\n     * Check if the access token is considered as expired.\n     *\n     * The access token is considered expired if the time specified with [REFRESH_PRIOR_TIME] has elapsed before the expiry date.\n     */\n    private fun LocalAccessToken.isAccessTokenConsideredExpired(): Boolean {\n        val expirationTime = getExpirationTimeInSeconds().seconds\n        return System.currentTimeMillis() >= (expirationTime - REFRESH_PRIOR_TIME).inWholeMilliseconds\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/auth/RefreshAccessTokenUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.logI\nimport retrofit2.HttpException\n\nclass RefreshAccessTokenUseCase(\n    private val accessTokenRepository: AccessTokenRepository,\n    private val userRepository: UserRepository,\n    private val logOutUser: LogOutUserUseCase\n) {\n\n    suspend operator fun invoke(): RefreshResult {\n        logI(\"Refresh: Refreshing access token.\")\n        val accessToken = try {\n            accessTokenRepository.refreshAccessToken()\n        } catch (e: HttpException) {\n            // trigger logout if the refresh token is invalid\n            if (e.code() in 400..499) {\n                logE(\"Refresh: Failed with status code ${e.code()}. Triggering logout...\", e)\n                triggerLogOutWithLoginPrompt()\n                return RefreshResult.Failure\n            }\n\n            logE(\"Refresh: Failed to refresh access token.\", e)\n            return RefreshResult.Error(e)\n        } catch (e: Exception) {\n            logE(\"Refresh: Failed to refresh access token.\", e)\n            return RefreshResult.Error(e)\n        }\n\n        logI(\"Refresh: Successfully refreshed access token.\")\n        return RefreshResult.Success(accessToken)\n    }\n\n    /**\n     * Log out the current user and prompt for re-login.\n     */\n    private suspend fun triggerLogOutWithLoginPrompt() {\n        logOutUser()\n        userRepository.promptUserReLogIn()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/auth/RefreshResult.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\n\nsealed interface RefreshResult {\n    /** The access token was successfully refreshed. */\n    data class Success(val accessToken: LocalAccessToken) : RefreshResult\n\n    /** The access token could not be refreshed, e.g. invalid or expired refresh token. */\n    data object Failure : RefreshResult\n\n    /** Refresh failed due to an error. */\n    data class Error(val exception: Exception) : RefreshResult\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/library/FetchLibraryEntriesForWidgetUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.library.LibraryEntryKind\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryFilter\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.domain.user.GetLocalUserIdUseCase\nimport io.github.drumber.kitsune.util.logE\n\nclass FetchLibraryEntriesForWidgetUseCase(\n    private val libraryRepository: LibraryRepository,\n    private val getLocalUserId: GetLocalUserIdUseCase\n) {\n\n    suspend operator fun invoke(count: Int) {\n        val userId = getLocalUserId() ?: return\n\n        val requestFilter = Filter()\n            .filter(\"user_id\", userId)\n            .sort(\"status\", \"-progressed_at\")\n            .include(\"anime\", \"manga\")\n\n        val filter = LibraryEntryFilter(\n            kind = LibraryEntryKind.All,\n            libraryStatus = listOf(LibraryStatus.Current),\n            initialFilter = requestFilter\n        ).pageSize(count)\n\n        try {\n            libraryRepository.fetchAndStoreLibraryEntriesForFilter(filter)\n        } catch (e: Exception) {\n            logE(\"Failed to fetch library entries for widget\", e)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/library/GetLibraryEntriesWithModificationsPagerUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport androidx.paging.PagingData\nimport androidx.paging.cachedIn\nimport androidx.paging.map\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryFilter\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.combine\n\nclass GetLibraryEntriesWithModificationsPagerUseCase(\n    private val libraryRepository: LibraryRepository\n) {\n\n    operator fun invoke(\n        pageSize: Int,\n        filter: LibraryEntryFilter,\n        cacheScope: CoroutineScope\n    ): Flow<PagingData<LibraryEntryWithModification>> {\n        return libraryRepository.libraryEntriesPager(pageSize, filter)\n            .cachedIn(cacheScope)\n            .combine(libraryRepository.getLibraryEntryModificationsAsFlow()) { pagingData, modifications ->\n                pagingData.map { entry ->\n                    LibraryEntryWithModification(\n                        entry,\n                        modifications.find { it.id == entry.id }\n                    )\n                }\n            }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/library/LibraryEntryUpdateResult.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\n\nsealed class LibraryEntryUpdateResult {\n    data class Success(val updatedLibraryEntry: LibraryEntry) : LibraryEntryUpdateResult()\n    data class Failure(val reason: LibraryEntryUpdateFailureReason) : LibraryEntryUpdateResult()\n}\n\nsealed class LibraryEntryUpdateFailureReason {\n    data object NotFound : LibraryEntryUpdateFailureReason()\n    data class NetworkError(val exception: Exception) : LibraryEntryUpdateFailureReason()\n    data class UnknownException(val exception: Exception) : LibraryEntryUpdateFailureReason()\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/library/SearchLibraryEntriesWithLocalModificationsPagerUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport androidx.paging.PagingData\nimport androidx.paging.cachedIn\nimport androidx.paging.map\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.combine\n\nclass SearchLibraryEntriesWithLocalModificationsPagerUseCase(\n    private val libraryRepository: LibraryRepository\n) {\n\n    operator fun invoke(\n        pageSize: Int,\n        filter: Filter,\n        cacheScope: CoroutineScope\n    ): Flow<PagingData<LibraryEntryWithModification>> {\n        return libraryRepository.searchLibraryEntriesPager(pageSize, filter)\n            .cachedIn(cacheScope)\n            .combine(libraryRepository.getLibraryEntryModificationsAsFlow()) { pagingData, modifications ->\n                pagingData.map { entry ->\n                    LibraryEntryWithModification(\n                        entry,\n                        modifications.find { it.id == entry.id }\n                    )\n                }\n            }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/library/SynchronizeLocalLibraryModificationsUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\n\nclass SynchronizeLocalLibraryModificationsUseCase(\n    private val libraryRepository: LibraryRepository,\n    private val updateLibraryEntry: UpdateLibraryEntryUseCase\n) {\n\n    suspend operator fun invoke(): Map<String, LibraryEntryUpdateResult> {\n        return libraryRepository.getAllLibraryEntryModifications()\n            .associate { libraryEntryModification ->\n                libraryEntryModification.id to updateLibraryEntry(\n                    libraryEntryModification\n                )\n            }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/library/UpdateLibraryEntryProgressUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.util.formatUtcDate\nimport io.github.drumber.kitsune.util.getLocalCalendar\n\nclass UpdateLibraryEntryProgressUseCase(\n    private val updateLibraryEntry: UpdateLibraryEntryUseCase\n) {\n\n    suspend operator fun invoke(\n        libraryEntry: LibraryEntry,\n        newProgress: Int\n    ): LibraryEntryUpdateResult {\n        var modification = LibraryEntryModification.withIdAndNulls(libraryEntry.id)\n            .copy(progress = newProgress)\n\n        // set startedAt date when starting consuming library entry\n        if (\n            libraryEntry.startedAt.isNullOrBlank() &&\n            newProgress == 1 &&\n            (libraryEntry.progress ?: 0) == 0\n        ) {\n            modification = modification.copy(\n                startedAt = getLocalCalendar().formatUtcDate()\n            )\n        }\n\n        return updateLibraryEntry(modification)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/library/UpdateLibraryEntryRatingUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\n\nclass UpdateLibraryEntryRatingUseCase(\n    private val updateLibraryEntry: UpdateLibraryEntryUseCase\n) {\n\n    suspend operator fun invoke(\n        libraryEntry: LibraryEntry,\n        rating: Int?\n    ): LibraryEntryUpdateResult {\n        val updatedRating = rating ?: -1 // '-1' will be mapped to 'null' by the json serializer\n\n        val modification = LibraryEntryModification.withIdAndNulls(libraryEntry.id)\n            .copy(ratingTwenty = updatedRating)\n\n        return updateLibraryEntry(modification)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/library/UpdateLibraryEntryUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateFailureReason.NetworkError\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateFailureReason.NotFound\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateFailureReason.UnknownException\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult.Failure\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult.Success\nimport io.github.drumber.kitsune.data.common.exception.NotFoundException\nimport java.io.IOException\nimport kotlin.coroutines.cancellation.CancellationException\n\nclass UpdateLibraryEntryUseCase(\n    private val libraryRepository: LibraryRepository\n) {\n\n    suspend operator fun invoke(modification: LibraryEntryModification): LibraryEntryUpdateResult {\n        return try {\n            val updatedLibraryEntry = libraryRepository.updateLibraryEntry(modification)\n            Success(updatedLibraryEntry)\n        } catch (e: CancellationException) {\n            throw e\n        } catch (e: NotFoundException) {\n            Failure(NotFound)\n        } catch (e: IOException) {\n            Failure(NetworkError(e))\n        } catch (e: Exception) {\n            Failure(UnknownException(e))\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/user/GetLocalUserIdUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.user\n\nimport io.github.drumber.kitsune.data.repository.UserRepository\n\nclass GetLocalUserIdUseCase(\n    private val userRepository: UserRepository\n) {\n\n    operator fun invoke(): String? {\n        return userRepository.localUser.value?.id\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/user/UpdateLocalUserUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.user\n\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.domain.auth.IsUserLoggedInUseCase\nimport io.github.drumber.kitsune.domain.auth.RefreshAccessTokenIfExpiredUseCase\nimport io.github.drumber.kitsune.domain.auth.RefreshResult\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.logI\n\nclass UpdateLocalUserUseCase(\n    private val userRepository: UserRepository,\n    private val isUserLoggedIn: IsUserLoggedInUseCase,\n    private val refreshAccessTokenIfExpired: RefreshAccessTokenIfExpiredUseCase\n) {\n\n    suspend operator fun invoke() {\n        if (!isUserLoggedIn()) {\n            logI(\"Cannot update local user: User is not logged in.\")\n            return\n        }\n\n        val refreshResult = refreshAccessTokenIfExpired()\n        if (refreshResult != null && refreshResult !is RefreshResult.Success) {\n            logI(\"Cannot update local user: Access token refresh failed.\")\n            return\n        }\n\n        try {\n            userRepository.fetchAndStoreLocalUserFromNetwork()\n        } catch (e: Exception) {\n            logE(\"Failed to update local user model from network.\", e)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/domain/work/UpdateLibraryWidgetUseCase.kt",
    "content": "package io.github.drumber.kitsune.domain.work\n\nimport android.content.Context\nimport androidx.work.ExistingWorkPolicy\nimport androidx.work.OneTimeWorkRequestBuilder\nimport androidx.work.WorkManager\nimport io.github.drumber.kitsune.work.UpdateLibraryWidgetWorker\n\nclass UpdateLibraryWidgetUseCase {\n\n    operator fun invoke(context: Context) {\n        val updateWidgetWork = OneTimeWorkRequestBuilder<UpdateLibraryWidgetWorker>()\n            .build()\n\n        WorkManager.getInstance(context).enqueueUniqueWork(\n            UpdateLibraryWidgetWorker.TAG,\n            ExistingWorkPolicy.REPLACE,\n            updateWidgetWork\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/notification/NotificationChannels.kt",
    "content": "package io.github.drumber.kitsune.notification\n\nimport android.content.Context\nimport androidx.core.app.NotificationChannelCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH\nimport io.github.drumber.kitsune.R\n\nobject NotificationChannels {\n\n    const val CHANNEL_UPDATE_CHECKER = \"update_checker\"\n    const val ID_NEW_VERSION = 10\n\n    fun registerNotificationChannels(context: Context) {\n        val notificationManager = NotificationManagerCompat.from(context)\n        notificationManager.createNotificationChannelsCompat(\n            listOf(\n                notificationChannel(CHANNEL_UPDATE_CHECKER, IMPORTANCE_HIGH) {\n                    setName(context.getString(R.string.notification_updates))\n                    setShowBadge(false)\n                }\n            )\n        )\n    }\n\n    private fun notificationChannel(\n        id: String,\n        importance: Int,\n        block: NotificationChannelCompat.Builder.() -> Unit\n    ): NotificationChannelCompat {\n        return NotificationChannelCompat.Builder(id, importance).apply(block).build()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/notification/Notifications.kt",
    "content": "package io.github.drumber.kitsune.notification\n\nimport android.Manifest\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport androidx.core.app.ActivityCompat\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.appupdate.AppRelease\nimport io.github.drumber.kitsune.notification.NotificationChannels.CHANNEL_UPDATE_CHECKER\n\nobject Notifications {\n\n    fun showNewVersion(context: Context, release: AppRelease) {\n        val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(release.url))\n        val releaseIntent = PendingIntent.getActivity(\n            context,\n            0,\n            browserIntent,\n            PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n        val notification = NotificationCompat.Builder(context, CHANNEL_UPDATE_CHECKER)\n            .setSmallIcon(R.drawable.ic_notification_icon)\n            .setContentTitle(context.getString(R.string.info_update_new_version_available))\n            .setContentText(context.getString(R.string.info_update_new_version_available_text, release.version))\n            .setPriority(NotificationCompat.PRIORITY_DEFAULT)\n            .setContentIntent(releaseIntent)\n            .setAutoCancel(true)\n            .build()\n\n        if (ActivityCompat.checkSelfPermission(\n                context,\n                Manifest.permission.POST_NOTIFICATIONS\n            ) != PackageManager.PERMISSION_GRANTED\n        ) {\n            return\n        }\n        NotificationManagerCompat.from(context).notify(NotificationChannels.ID_NEW_VERSION, notification)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/preference/CategoryPrefWrapper.kt",
    "content": "package io.github.drumber.kitsune.preference\n\ndata class CategoryPrefWrapper(\n    val categoryId: String? = null,\n    val categoryName: String? = null,\n    val categorySlug: String? = null,\n    val parentIds: List<String>? = null\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/preference/KitsunePref.kt",
    "content": "package io.github.drumber.kitsune.preference\n\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.lifecycle.asFlow\nimport com.chibatching.kotpref.KotprefModel\nimport com.chibatching.kotpref.enumpref.enumOrdinalPref\nimport com.chibatching.kotpref.enumpref.enumValuePref\nimport com.chibatching.kotpref.livedata.asLiveData\nimport com.fasterxml.jackson.core.JsonProcessingException\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.AppTheme\nimport io.github.drumber.kitsune.constants.MediaItemSize\nimport io.github.drumber.kitsune.data.common.library.LibraryEntryKind\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalTitleLanguagePreference\nimport io.github.drumber.kitsune.domain.algolia.FilterCollection\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.flow.combine\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.get\nimport org.koin.core.component.inject\nimport kotlin.reflect.KProperty\n\nobject KitsunePref : KotprefModel(), KoinComponent {\n\n    override val commitAllPropertiesByDefault = true\n    override val kotprefName = context.getString(R.string.preference_file_key)\n\n    private val userRepository: UserRepository by inject()\n\n    private var titlesIntern by enumValuePref(\n        LocalTitleLanguagePreference.Canonical,\n        key = R.string.preference_key_titles\n    )\n\n    var titles: LocalTitleLanguagePreference\n        set(value) {\n            titlesIntern = value\n        }\n        get() {\n            return userRepository.localUser.value?.titleLanguagePreference ?: titlesIntern\n        }\n\n    var appTheme by enumValuePref(AppTheme.DEFAULT)\n\n    var useDynamicColorTheme by booleanPref(\n        false,\n        key = R.string.preference_key_dynamic_color_theme\n    )\n\n    var darkMode by stringPref(\n        AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM.toString(),\n        key = R.string.preference_key_dark_mode\n    )\n\n    var oledBlackMode by booleanPref(\n        false,\n        key = R.string.preference_key_oled_black_mode\n    )\n\n    var mediaItemSize by enumOrdinalPref(MediaItemSize.MEDIUM)\n\n    var startFragment by enumValuePref(StartPagePref.Home)\n\n    var rememberSearchFilters by booleanPref(\n        true,\n        key = R.string.preference_key_remember_search_filters\n    )\n\n    var forceLegacyImagePicker by booleanPref(\n        false,\n        key = R.string.preference_key_force_legacy_image_picker\n    )\n\n\n    var checkForUpdatesOnStart by booleanPref(\n        false,\n        key = R.string.preference_key_check_for_updates_on_start\n    )\n\n\n    private var searchFiltersJson by stringPref(\"{}\")\n\n    var searchFilters: FilterCollection\n        set(value) {\n            searchFiltersJson = value.toJsonString()\n        }\n        get() = ::searchFiltersJson.fromJsonString(FilterCollection())\n\n\n    private var searchCategoriesJson by stringPref(\"[]\")\n\n    var searchCategories: List<CategoryPrefWrapper>\n        set(value) {\n            searchCategoriesJson = value.toJsonString()\n        }\n        get() = ::searchCategoriesJson.fromJsonString(emptyList())\n\n\n    var libraryEntryKind by enumValuePref(LibraryEntryKind.All)\n\n    private var libraryEntryStatusJson by stringPref(\"[]\")\n\n    var libraryEntryStatus: List<LibraryStatus>\n        set(value) {\n            libraryEntryStatusJson = value.toJsonString()\n        }\n        get() = ::libraryEntryStatusJson.fromJsonString(emptyList())\n\n\n    var flagUserDeniedNotificationPermission by booleanPref(false)\n\n\n    var ratingChartRatingSystem by enumValuePref(LocalRatingSystemPreference.Regular)\n\n\n    var lastLibraryFetchForWidget by longPref(default = -1L)\n\n\n    var lastUpdateCheck by longPref(default = -1L)\n\n\n    var onboardingFinishedVersionCode by intPref(default = -1)\n\n\n    private fun Any.toJsonString(): String {\n        val objectMapper: ObjectMapper = get()\n        return objectMapper.writeValueAsString(this)\n    }\n\n    private inline fun <reified T> KProperty<*>.fromJsonString(defaultValue: T): T {\n        val value = preferences.getString(getPrefKey(this), null) ?: return defaultValue\n        val objectMapper: ObjectMapper = get()\n        return try {\n            objectMapper.readValue(value)\n        } catch (e: JsonProcessingException) {\n            logE(\"Failed to parse object from JSON. Returning default value.\", e)\n            remove(this) // reset preference\n            defaultValue\n        }\n    }\n\n    fun getTitleLanguageAsFlow() = KitsunePref.asLiveData(KitsunePref::titlesIntern)\n        .asFlow()\n        .combine(userRepository.localUser) { preference, localUser ->\n            localUser?.titleLanguagePreference ?: preference\n        }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/preference/StartPagePref.kt",
    "content": "package io.github.drumber.kitsune.preference\n\nimport io.github.drumber.kitsune.R\n\nenum class StartPagePref {\n    Home,\n    Search,\n    Library,\n    Profile\n}\n\nfun StartPagePref.getDestinationId() = when (this) {\n    StartPagePref.Home -> R.id.main_fragment\n    StartPagePref.Search -> R.id.search_fragment\n    StartPagePref.Library -> R.id.library_fragment\n    StartPagePref.Profile -> R.id.profile_fragment\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/AbstractMediaRecyclerViewAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport androidx.core.view.ViewCompat\nimport androidx.databinding.OnRebindCallback\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.databinding.ItemMediaBinding\nimport io.github.drumber.kitsune.ui.adapter.AbstractMediaRecyclerViewAdapter.AbstractMediaViewHolder\nimport java.util.concurrent.CopyOnWriteArrayList\n\nabstract class AbstractMediaRecyclerViewAdapter<ViewHolder : AbstractMediaViewHolder<Item>, Item>(\n    val dataSet: CopyOnWriteArrayList<Item>,\n    private val glide: RequestManager,\n    private val transitionNameSuffix: String?,\n    private val listener: OnItemClickListener<Item>?\n) : RecyclerView.Adapter<ViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val binding = ItemMediaBinding.inflate(\n            LayoutInflater.from(parent.context),\n            parent,\n            false\n        )\n        binding.apply {\n            contentWrapper.layoutParams.width = FrameLayout.LayoutParams.WRAP_CONTENT\n            cardMedia.isInGridLayout = false\n        }\n\n        binding.addOnRebindCallback(object : OnRebindCallback<ItemMediaBinding>() {\n            override fun onBound(binding: ItemMediaBinding) {\n                // If the same media (with the same ID) is shown in more than one recyclerview,\n                // the shared-element-transition won't work. To make the transition name unique again,\n                // we may add a suffix (e.g. the section title on the home page).\n                transitionNameSuffix?.let { binding.cardMedia.fixUniqueTransitionName(it) }\n            }\n        })\n\n        return onCreateViewHolder(binding, glide) { view, position ->\n            if (position < dataSet.size) {\n                listener?.onItemClick(view, dataSet[position])\n            }\n        }\n    }\n\n    abstract fun onCreateViewHolder(\n        binding: ItemMediaBinding,\n        glide: RequestManager,\n        listener: (View, Int) -> Unit\n    ): ViewHolder\n\n    /** Add a suffix to the transitionName if not already present. */\n    private fun View.fixUniqueTransitionName(suffix: String) {\n        ViewCompat.getTransitionName(this)?.let { transitionName ->\n            if (!transitionName.endsWith(suffix))\n                ViewCompat.setTransitionName(this, transitionName + suffix)\n        }\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        holder.bind(dataSet[position])\n    }\n\n    override fun getItemCount() = dataSet.size\n\n    abstract class AbstractMediaViewHolder<T>(\n        binding: ItemMediaBinding,\n        onClick: (View, Int) -> Unit\n    ) : RecyclerView.ViewHolder(binding.root) {\n\n        init {\n            binding.cardMedia.setOnClickListener {\n                val position = bindingAdapterPosition\n                if (position != RecyclerView.NO_POSITION) {\n                    onClick(it, position)\n                }\n            }\n        }\n\n        abstract fun bind(data: T)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/CharacterAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.databinding.ItemSingleCharacterBinding\nimport java.util.concurrent.CopyOnWriteArrayList\n\nclass CharacterAdapter(\n    val dataSet: CopyOnWriteArrayList<Character>,\n    private val glide: RequestManager,\n    private val listener: OnItemClickListener<Character>? = null\n) : RecyclerView.Adapter<CharacterAdapter.SingleCharacterViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SingleCharacterViewHolder {\n        return SingleCharacterViewHolder(\n            ItemSingleCharacterBinding.inflate(\n                LayoutInflater.from(parent.context),\n                parent,\n                false\n            )\n        )\n    }\n\n    override fun onBindViewHolder(holder: SingleCharacterViewHolder, position: Int) {\n        holder.bind(dataSet[position])\n    }\n\n    override fun getItemCount() = dataSet.size\n\n    inner class SingleCharacterViewHolder(private val binding: ItemSingleCharacterBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        fun bind(character: Character) {\n            binding.cardCharacter.setOnClickListener {\n                listener?.onItemClick(binding.cardCharacter, character)\n            }\n\n            glide.load(character.image?.originalOrDown())\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivCharacter)\n\n            binding.tvName.text = character.name\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/MediaCharacterAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.MediaItemSize\nimport io.github.drumber.kitsune.data.presentation.model.character.MediaCharacter\nimport io.github.drumber.kitsune.data.presentation.model.character.getStringRes\nimport io.github.drumber.kitsune.databinding.ItemMediaBinding\nimport java.util.concurrent.CopyOnWriteArrayList\n\nclass MediaCharacterAdapter(\n    val dataSet: CopyOnWriteArrayList<MediaCharacter>,\n    private val glide: RequestManager,\n    private val listener: OnItemClickListener<MediaCharacter>? = null\n) : RecyclerView.Adapter<MediaCharacterAdapter.MediaCharacterViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaCharacterViewHolder {\n        return MediaCharacterViewHolder(\n            ItemMediaBinding.inflate(\n                LayoutInflater.from(parent.context),\n                parent,\n                false\n            )\n        )\n    }\n\n    override fun onBindViewHolder(holder: MediaCharacterViewHolder, position: Int) {\n        holder.bind(dataSet[position])\n    }\n\n    override fun getItemCount() = dataSet.size\n\n    inner class MediaCharacterViewHolder(private val binding: ItemMediaBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        init {\n            binding.apply {\n                contentWrapper.layoutParams.width = FrameLayout.LayoutParams.WRAP_CONTENT\n                cardMedia.setCustomItemSize(MediaItemSize.SMALL)\n                cardMedia.isInGridLayout = false\n            }\n        }\n\n        fun bind(data: MediaCharacter) {\n            val media = data.media\n            binding.data = media\n            binding.overlayTagText = data.role?.getStringRes()?.let { binding.root.context.getString(it) }\n\n            glide.load(media?.posterImageUrl)\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivThumbnail)\n\n            binding.cardMedia.setOnClickListener {\n                listener?.onItemClick(binding.cardMedia, data)\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/MediaMappingsAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.app.SearchManager\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.BaseAdapter\nimport com.bumptech.glide.Glide\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.mapping.Mapping\nimport io.github.drumber.kitsune.data.presentation.model.mapping.getExternalUrl\nimport io.github.drumber.kitsune.data.presentation.model.mapping.getSiteName\nimport io.github.drumber.kitsune.databinding.ItemMediaMappingBinding\nimport io.github.drumber.kitsune.util.extensions.copyToClipboard\nimport io.github.drumber.kitsune.util.extensions.showSomethingWrongToast\nimport io.github.drumber.kitsune.util.logD\nimport io.github.drumber.kitsune.util.logE\n\nclass MediaMappingsAdapter(\n    private val context: Context,\n    val dataSource: MutableList<Mapping>\n) : BaseAdapter() {\n\n    override fun getCount(): Int = dataSource.size\n\n    override fun getItem(position: Int) = dataSource[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 binding = if (convertView == null) {\n            ItemMediaMappingBinding.inflate(LayoutInflater.from(context), parent, false)\n        } else {\n            ItemMediaMappingBinding.bind(convertView)\n        }\n\n        val mapping = getItem(position)\n        binding.tvSiteName.text = mapping.getSiteName() ?: mapping.externalSite ?: \"?\"\n        binding.tvSiteUrl.text = mapping.getExternalUrl() ?: mapping.externalId ?: \"-\"\n\n        mapping.getExternalUrl()?.let { url ->\n            try {\n                val domain = Uri.parse(url).host\n                Glide.with(context)\n                    .load(\"https://icons.duckduckgo.com/ip3/$domain.ico\")\n                    .placeholder(R.drawable.ic_website)\n                    .into(binding.ivSiteIcon)\n            } catch (e: Exception) {\n                logD(\"Failed to load favicon for url: $url\", e)\n            }\n        }\n\n        binding.root.setOnClickListener {\n            val url = mapping.getExternalUrl()\n            if (url != null) {\n                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))\n                try {\n                    context.startActivity(intent)\n                } catch (e: Exception) {\n                    logE(\"Failed to open URL: $url\", e)\n                    context.showSomethingWrongToast()\n                }\n            } else {\n                // do a web search\n                val query = \"${mapping.externalSite} ${mapping.externalId}\"\n                val intent = Intent(Intent.ACTION_WEB_SEARCH)\n                intent.putExtra(SearchManager.QUERY, query)\n                try {\n                    context.startActivity(intent)\n                } catch (e: Exception) {\n                    logE(\"Failed to do a web search for: $query\", e)\n                    context.showSomethingWrongToast()\n                }\n            }\n        }\n\n        binding.root.setOnLongClickListener {\n            val url = mapping.getExternalUrl() ?: mapping.externalId ?: return@setOnLongClickListener false\n            context.copyToClipboard(mapping.getSiteName() ?: \"URL\", url)\n            return@setOnLongClickListener true\n        }\n\n        return binding.root\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/MediaRecyclerViewAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.View\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.constants.MediaItemSize\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.databinding.ItemMediaBinding\nimport java.util.concurrent.CopyOnWriteArrayList\n\nclass MediaRecyclerViewAdapter(\n    dataSet: CopyOnWriteArrayList<Media>,\n    glide: RequestManager,\n    private val showSubtype: Boolean = false,\n    private val itemSize: MediaItemSize? = null,\n    transitionNameSuffix: String? = null,\n    listener: OnItemClickListener<Media>? = null\n) : AbstractMediaRecyclerViewAdapter<MediaViewHolder, Media>(\n    dataSet,\n    glide,\n    transitionNameSuffix,\n    listener\n) {\n\n    var overrideItemSize: MediaItemSize? = null\n\n    override fun onCreateViewHolder(\n        binding: ItemMediaBinding,\n        glide: RequestManager,\n        listener: (View, Int) -> Unit\n    ): MediaViewHolder {\n        itemSize?.let { binding.cardMedia.setCustomItemSize(it) }\n\n        return MediaViewHolder(\n            binding,\n            glide,\n            showSubtype,\n            listener\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/MediaRelationshipRecyclerViewAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.View\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.constants.MediaItemSize\nimport io.github.drumber.kitsune.data.presentation.model.media.relationship.MediaRelationship\nimport io.github.drumber.kitsune.databinding.ItemMediaBinding\nimport java.util.concurrent.CopyOnWriteArrayList\n\nclass MediaRelationshipRecyclerViewAdapter(\n    dataSet: CopyOnWriteArrayList<MediaRelationship>,\n    glide: RequestManager,\n    transitionNameSuffix: String? = null,\n    listener: OnItemClickListener<MediaRelationship>? = null\n) : AbstractMediaRecyclerViewAdapter<MediaRelationshipViewHolder, MediaRelationship>(\n    dataSet,\n    glide,\n    transitionNameSuffix,\n    listener\n) {\n\n    override fun onCreateViewHolder(\n        binding: ItemMediaBinding,\n        glide: RequestManager,\n        listener: (View, Int) -> Unit\n    ): MediaRelationshipViewHolder {\n        binding.cardMedia.setCustomItemSize(MediaItemSize.SMALL)\n        return MediaRelationshipViewHolder(\n            binding,\n            glide,\n            listener\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/MediaRelationshipViewHolder.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.View\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.media.relationship.MediaRelationship\nimport io.github.drumber.kitsune.data.presentation.model.media.relationship.getStringRes\nimport io.github.drumber.kitsune.databinding.ItemMediaBinding\nimport io.github.drumber.kitsune.ui.adapter.AbstractMediaRecyclerViewAdapter.AbstractMediaViewHolder\n\nclass MediaRelationshipViewHolder(\n    private val binding: ItemMediaBinding,\n    private val glide: RequestManager,\n    onClick: (View, Int) -> Unit\n) : AbstractMediaViewHolder<MediaRelationship>(binding, onClick) {\n\n    override fun bind(data: MediaRelationship) {\n        binding.data = data.media\n        binding.overlayTagText = data.role?.getStringRes()\n            ?.let { binding.root.context.getString(it) }\n        data.media?.posterImageUrl?.let {\n            glide.load(it)\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivThumbnail)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/MediaViewHolder.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.View\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.databinding.ItemMediaBinding\nimport io.github.drumber.kitsune.util.fixImageUrl\n\nclass MediaViewHolder(\n    private val binding: ItemMediaBinding,\n    private val glide: RequestManager,\n    private val showSubtype: Boolean = false,\n    listener: (View, Int) -> Unit\n) : AbstractMediaRecyclerViewAdapter.AbstractMediaViewHolder<Media>(binding, listener) {\n\n    override fun bind(data: Media) {\n        binding.data = data\n        binding.overlayTagText = when (showSubtype) {\n            false -> null\n            true -> data.subtypeFormatted\n        }\n        glide.load(data.posterImageUrl?.fixImageUrl())\n            .placeholder(R.drawable.ic_insert_photo_48)\n            .into(binding.ivThumbnail)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/OnItemClickListener.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.View\n\nfun interface OnItemClickListener<T> {\n    fun onItemClick(view: View, item: T)\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/StreamingLinkAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.TooltipCompat\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.StreamingLogo\nimport io.github.drumber.kitsune.data.presentation.model.media.streamer.StreamingLink\nimport io.github.drumber.kitsune.databinding.ItemStreamerBinding\nimport java.util.concurrent.CopyOnWriteArrayList\n\nclass StreamingLinkAdapter(\n    val dataSet: CopyOnWriteArrayList<StreamingLink> = CopyOnWriteArrayList(),\n    private val glide: RequestManager,\n    private val listener: OnItemClickListener<StreamingLink>? = null\n) : RecyclerView.Adapter<StreamingLinkAdapter.StreamingLinkViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamingLinkViewHolder {\n        val binding = ItemStreamerBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        return StreamingLinkViewHolder(binding) { position ->\n            if (position < dataSet.size) {\n                listener?.onItemClick(binding.root, dataSet[position])\n            }\n        }\n    }\n\n    override fun onBindViewHolder(holder: StreamingLinkViewHolder, position: Int) {\n        holder.bind(dataSet[position])\n    }\n\n    override fun getItemCount() = dataSet.size\n\n    inner class StreamingLinkViewHolder(\n        private val binding: ItemStreamerBinding,\n        private val listener: (Int) -> Unit\n    ) : RecyclerView.ViewHolder(binding.root) {\n\n        init {\n            binding.root.setOnClickListener {\n                val position = bindingAdapterPosition\n                if(position != RecyclerView.NO_POSITION) {\n                    listener(position)\n                }\n            }\n        }\n\n        fun bind(streamingLink: StreamingLink) {\n            val logo = streamingLink.streamer?.siteName?.let { siteName ->\n                StreamingLogo.entries.find { it.name.equals(siteName, true) }?.drawable\n            }\n            glide.load(logo)\n                .centerInside()\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivLogo)\n\n            TooltipCompat.setTooltipText(binding.root, streamingLink.streamer?.siteName)\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/paging/AnimeAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter.paging\n\nimport androidx.recyclerview.widget.DiffUtil\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.ui.adapter.OnItemClickListener\n\nclass AnimeAdapter(glide: RequestManager, listener: OnItemClickListener<Anime>? = null) :\n    MediaPagingAdapter<Anime>(AnimeComparator, glide, listener) {\n\n    object AnimeComparator: DiffUtil.ItemCallback<Anime>() {\n        override fun areItemsTheSame(oldItem: Anime, newItem: Anime) = oldItem.id == newItem.id\n\n        override fun areContentsTheSame(oldItem: Anime, newItem: Anime) = oldItem == newItem\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/paging/CharacterPagingAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter.paging\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.paging.PagingDataAdapter\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.presentation.model.media.production.Casting\nimport io.github.drumber.kitsune.databinding.ItemCharacterBinding\nimport io.github.drumber.kitsune.ui.adapter.OnItemClickListener\n\nclass CharacterPagingAdapter(\n    private val glide: RequestManager,\n    private val characterClickListener: OnItemClickListener<Character>? = null\n) : PagingDataAdapter<Casting, CharacterPagingAdapter.CharacterViewHolder>(CharacterComparator) {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {\n        return CharacterViewHolder(\n            ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        )\n    }\n\n    override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {\n        if (position >= itemCount) return\n        getItem(position)?.let { holder.bind(it) }\n    }\n\n    inner class CharacterViewHolder(private val binding: ItemCharacterBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        fun bind(casting: Casting) {\n            val imgCharacter = casting.character?.image?.original\n            val imgActor = casting.person?.image?.original\n\n            glide.load(imgCharacter)\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivCharacter)\n\n            glide.load(imgActor)\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivActor)\n\n            binding.apply {\n                tvCharacterName.text = casting.character?.name\n                tvActorName.text = casting.person?.name\n\n                ivCharacter.isVisible = imgCharacter != null || !tvCharacterName.text.isNullOrBlank()\n                ivActor.isVisible = imgActor != null || !tvActorName.text.isNullOrBlank()\n\n                ivCharacter.setOnClickListener {\n                    casting.character?.let { character ->\n                        characterClickListener?.onItemClick(ivCharacter, character)\n                    }\n                }\n            }\n        }\n\n    }\n\n    object CharacterComparator : DiffUtil.ItemCallback<Casting>() {\n        override fun areItemsTheSame(oldItem: Casting, newItem: Casting) = oldItem.id == newItem.id\n\n        override fun areContentsTheSame(oldItem: Casting, newItem: Casting) = oldItem == newItem\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/paging/LibraryEntriesAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter.paging\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.paging.PagingDataAdapter\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryUiModel\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryUiModel.EntryModel\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryUiModel.StatusSeparatorModel\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.presentation.model.library.getStringResId\nimport io.github.drumber.kitsune.databinding.ItemLibraryEntryBinding\nimport io.github.drumber.kitsune.databinding.ItemLibraryStatusSeparatorBinding\n\nclass LibraryEntriesAdapter(\n    private val glide: RequestManager,\n    private val listener: LibraryEntryActionListener? = null\n) : PagingDataAdapter<LibraryEntryUiModel, RecyclerView.ViewHolder>(LibraryEntryUiModelComparator) {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {\n        R.layout.item_library_entry -> LibraryEntryViewHolder(\n            ItemLibraryEntryBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        )\n        else -> StatusSeparatorViewHolder(\n            ItemLibraryStatusSeparatorBinding.inflate(\n                LayoutInflater.from(parent.context),\n                parent,\n                false\n            )\n        )\n    }\n\n    override fun getItemViewType(position: Int): Int {\n        if (position >= itemCount) return 0\n        return when (peek(position)) {\n            is EntryModel -> R.layout.item_library_entry\n            is StatusSeparatorModel -> R.layout.item_library_status_separator\n            else -> 0\n        }\n    }\n\n    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {\n        if (position >= itemCount) return\n        val item = getItem(position) ?: return\n        when (holder) {\n            is LibraryEntryViewHolder -> holder.bind((item as EntryModel).entry)\n            is StatusSeparatorViewHolder -> holder.bind(item as StatusSeparatorModel)\n        }\n    }\n\n    inner class LibraryEntryViewHolder(\n        private val binding: ItemLibraryEntryBinding\n    ) : RecyclerView.ViewHolder(binding.root) {\n\n        init {\n            binding.apply {\n                cardView.setOnClickListener {\n                    if (bindingAdapterPosition != RecyclerView.NO_POSITION) {\n                        getItem(bindingAdapterPosition)?.let { listener?.onItemClicked(cardView, (it as EntryModel).entry) }\n                    }\n                }\n                cardView.setOnLongClickListener {\n                    if (bindingAdapterPosition != RecyclerView.NO_POSITION) {\n                        getItem(bindingAdapterPosition)?.let { listener?.onItemLongClicked((it as EntryModel).entry) }\n                        return@setOnLongClickListener true\n                    }\n                    return@setOnLongClickListener false\n                }\n                btnWatchedAdd.setOnClickListener {\n                    if (bindingAdapterPosition != RecyclerView.NO_POSITION) {\n                        getItem(bindingAdapterPosition)?.let { listener?.onEpisodeWatchedClicked((it as EntryModel).entry) }\n                    }\n                }\n                btnWatchedRemoved.setOnClickListener {\n                    if (bindingAdapterPosition != RecyclerView.NO_POSITION) {\n                        getItem(bindingAdapterPosition)?.let {\n                            listener?.onEpisodeUnwatchedClicked((it as EntryModel).entry)\n                        }\n                    }\n                }\n                btnRating.setOnClickListener {\n                    if (bindingAdapterPosition != RecyclerView.NO_POSITION) {\n                        getItem(bindingAdapterPosition)?.let {\n                            listener?.onRatingClicked((it as EntryModel).entry)\n                        }\n                    }\n                }\n            }\n        }\n\n        fun bind(libraryEntry: LibraryEntryWithModification) {\n            binding.entry = libraryEntry\n\n            glide.load(libraryEntry.media?.posterImageUrl)\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivThumbnail)\n\n            binding.tvNotSynced.isVisible = libraryEntry.isNotSynced\n            binding.tvTitle.maxLines = if (libraryEntry.isNotSynced) 2 else 3\n        }\n    }\n\n    inner class StatusSeparatorViewHolder(\n        private val binding: ItemLibraryStatusSeparatorBinding\n    ) : RecyclerView.ViewHolder(binding.root) {\n\n        fun bind(statusSeparator: StatusSeparatorModel) {\n            binding.tvTitle.setText(statusSeparator.status.getStringResId(!statusSeparator.isMangaSelected))\n        }\n\n    }\n\n    object LibraryEntryUiModelComparator : DiffUtil.ItemCallback<LibraryEntryUiModel>() {\n        override fun areItemsTheSame(\n            oldItem: LibraryEntryUiModel,\n            newItem: LibraryEntryUiModel\n        ): Boolean {\n            val isSameLibraryEntry = oldItem is EntryModel\n                    && newItem is EntryModel\n                    && oldItem.entry.id == newItem.entry.id\n\n            val isSameSeparator = oldItem is StatusSeparatorModel\n                    && newItem is StatusSeparatorModel\n                    && oldItem.status == newItem.status\n\n            return isSameLibraryEntry || isSameSeparator\n        }\n\n\n        override fun areContentsTheSame(\n            oldItem: LibraryEntryUiModel,\n            newItem: LibraryEntryUiModel\n        ) = oldItem == newItem\n    }\n\n    interface LibraryEntryActionListener {\n        fun onItemClicked(view: View, item: LibraryEntryWithModification)\n        fun onItemLongClicked(item: LibraryEntryWithModification)\n        fun onEpisodeWatchedClicked(item: LibraryEntryWithModification)\n        fun onEpisodeUnwatchedClicked(item: LibraryEntryWithModification)\n        fun onRatingClicked(item: LibraryEntryWithModification)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/paging/MangaAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter.paging\n\nimport androidx.recyclerview.widget.DiffUtil\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.ui.adapter.OnItemClickListener\n\nclass MangaAdapter(glide: RequestManager, listener: OnItemClickListener<Manga>? = null) :\n    MediaPagingAdapter<Manga>(MangaComparator, glide, listener) {\n\n    object MangaComparator: DiffUtil.ItemCallback<Manga>() {\n        override fun areItemsTheSame(oldItem: Manga, newItem: Manga) = oldItem.id == newItem.id\n\n        override fun areContentsTheSame(oldItem: Manga, newItem: Manga) = oldItem == newItem\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/paging/MediaPagingAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter.paging\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.paging.PagingDataAdapter\nimport androidx.recyclerview.widget.DiffUtil\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.databinding.ItemMediaBinding\nimport io.github.drumber.kitsune.ui.adapter.MediaViewHolder\nimport io.github.drumber.kitsune.ui.adapter.OnItemClickListener\n\nsealed class MediaPagingAdapter<T : Media>(\n    diffCallback: DiffUtil.ItemCallback<T>,\n    private val glide: RequestManager,\n    private val listener: OnItemClickListener<T>? = null\n) : PagingDataAdapter<T, MediaViewHolder>(diffCallback) {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {\n        val binding = ItemMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        binding.cardMedia.isInGridLayout = true\n\n        return MediaViewHolder(\n            binding,\n            glide\n        ) { _, position ->\n            getItem(position)?.let { item -> listener?.onItemClick(binding.cardMedia, item) }\n        }\n    }\n\n    override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {\n        if (position >= itemCount) return\n        getItem(position)?.let { holder.bind(it) }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/paging/MediaSearchPagingAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter.paging\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.paging.PagingDataAdapter\nimport androidx.recyclerview.widget.DiffUtil\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.databinding.ItemMediaBinding\nimport io.github.drumber.kitsune.ui.adapter.MediaViewHolder\nimport io.github.drumber.kitsune.ui.adapter.OnItemClickListener\n\nclass MediaSearchPagingAdapter(\n    private val glide: RequestManager,\n    private val listener: OnItemClickListener<Media>? = null\n) : PagingDataAdapter<Media, MediaViewHolder>(MediaSearchComparator) {\n\n    override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {\n        if (position >= itemCount) return\n        getItem(position)?.let { holder.bind(it) }\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {\n        val binding = ItemMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        binding.cardMedia.isInGridLayout = true\n\n        return MediaViewHolder(\n            binding,\n            glide,\n            showSubtype = true\n        ) { _, position ->\n            getItem(position)?.let { item -> listener?.onItemClick(binding.cardMedia, item) }\n        }\n    }\n\n    object MediaSearchComparator : DiffUtil.ItemCallback<Media>() {\n        override fun areItemsTheSame(oldItem: Media, newItem: Media) =\n            oldItem.mediaType == newItem.mediaType && oldItem.id == newItem.id\n\n        override fun areContentsTheSame(oldItem: Media, newItem: Media) =\n            oldItem == newItem\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/paging/MediaUnitPagingAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter.paging\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.paging.PagingDataAdapter\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.media.unit.MediaUnit\nimport io.github.drumber.kitsune.databinding.ItemEpisodeBinding\nimport kotlin.math.max\n\nclass MediaUnitPagingAdapter(\n    private val glide: RequestManager,\n    private val posterUrl: String?,\n    private var enableWatchedCheckbox: Boolean,\n    private val listener: MediaUnitActionListener\n) : PagingDataAdapter<MediaUnit, MediaUnitPagingAdapter.MediaUnitViewHolder>(MediaUnitComparator) {\n\n    private var numberWatched = 0\n\n    fun updateLibraryWatchCount(numberWatched: Int) {\n        val oldValue = this.numberWatched\n        this.numberWatched = numberWatched\n        notifyItemRangeChanged(0, max(oldValue, numberWatched))\n    }\n\n    fun setIsWatchedCheckboxEnabled(isEnabled: Boolean) {\n        if (isEnabled == enableWatchedCheckbox) return\n        enableWatchedCheckbox = isEnabled\n        notifyItemRangeChanged(0, itemCount)\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaUnitViewHolder {\n        return MediaUnitViewHolder(\n            ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        )\n    }\n\n    override fun onBindViewHolder(holder: MediaUnitViewHolder, position: Int) {\n        if (position >= itemCount) return\n        getItem(position)?.let { holder.bind(it) }\n    }\n\n    inner class MediaUnitViewHolder(private val binding: ItemEpisodeBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        init {\n            binding.root.setOnClickListener {\n                if (bindingAdapterPosition != RecyclerView.NO_POSITION) {\n                    getItem(bindingAdapterPosition)?.let { listener.onMediaUnitClicked(it) }\n                }\n            }\n            binding.checkboxWatched.setOnClickListener {\n                val isChecked = binding.checkboxWatched.isChecked\n                getItem(bindingAdapterPosition)?.let { listener.onWatchStateChanged(it, isChecked) }\n            }\n        }\n\n        fun bind(unit: MediaUnit) {\n            glide.load(unit.thumbnail?.originalOrDown() ?: posterUrl)\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivThumbnail)\n\n            binding.apply {\n                tvEpisodeTitle.text = unit.title(root.context)\n                tvEpisodeNumber.text = if (unit.hasValidTitle()) {\n                    unit.numberText(root.context)\n                } else {\n                    null\n                }\n                checkboxWatched.isVisible = enableWatchedCheckbox\n                unit.number?.let { checkboxWatched.isChecked = it <= numberWatched }\n            }\n        }\n\n    }\n\n    interface MediaUnitActionListener {\n        fun onMediaUnitClicked(mediaUnit: MediaUnit)\n        fun onWatchStateChanged(mediaUnit: MediaUnit, isWatched: Boolean)\n    }\n\n    object MediaUnitComparator : DiffUtil.ItemCallback<MediaUnit>() {\n        override fun areItemsTheSame(oldItem: MediaUnit, newItem: MediaUnit) =\n            oldItem.id == newItem.id\n\n        override fun areContentsTheSame(oldItem: MediaUnit, newItem: MediaUnit) = oldItem == newItem\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/adapter/paging/ResourceLoadStateAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.adapter.paging\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.paging.LoadState\nimport androidx.paging.LoadStateAdapter\nimport androidx.paging.PagingDataAdapter\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager\nimport io.github.drumber.kitsune.databinding.ItemNetworkStateBinding\n\nclass ResourceLoadStateAdapter<T : Any, VH : RecyclerView.ViewHolder>(\n    private val adapter: PagingDataAdapter<T, VH>,\n) : LoadStateAdapter<ResourceLoadStateAdapter<T, VH>.NetworkStateItemViewHolder>() {\n\n    override fun onBindViewHolder(\n        holder: NetworkStateItemViewHolder,\n        loadState: LoadState\n    ) {\n        holder.bind(loadState)\n        // support full width for StaggeredGridLayout\n        (holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.isFullSpan = true\n    }\n\n    override fun onCreateViewHolder(\n        parent: ViewGroup,\n        loadState: LoadState\n    ): NetworkStateItemViewHolder {\n        return NetworkStateItemViewHolder(\n            ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        ) { adapter.retry() }\n    }\n\n\n    inner class NetworkStateItemViewHolder(\n        private val binding: ItemNetworkStateBinding,\n        private val retryCallback: () -> Unit\n    ) : RecyclerView.ViewHolder(binding.root) {\n\n        init {\n            binding.layoutLoading.btnRetry.setOnClickListener { retryCallback() }\n        }\n\n        fun bind(loadState: LoadState) {\n            with(binding.layoutLoading) {\n                progressBar.isVisible = loadState is LoadState.Loading\n                btnRetry.isVisible = loadState is LoadState.Error\n                tvError.isVisible = !(loadState as? LoadState.Error)?.error?.message.isNullOrBlank()\n                tvError.text = (loadState as? LoadState.Error)?.error?.message\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/authentication/AuthenticationActivity.kt",
    "content": "package io.github.drumber.kitsune.ui.authentication\n\nimport android.app.Activity\nimport android.os.Bundle\nimport android.text.Editable\nimport android.text.TextWatcher\nimport android.text.method.LinkMovementMethod\nimport android.view.inputmethod.EditorInfo\nimport android.widget.Toast\nimport androidx.annotation.StringRes\nimport androidx.core.view.isVisible\nimport com.google.android.material.textfield.TextInputLayout\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.ActivityAuthenticationBinding\nimport io.github.drumber.kitsune.ui.base.BaseActivity\nimport io.github.drumber.kitsune.util.ui.initImePaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass AuthenticationActivity : BaseActivity() {\n\n    companion object {\n        const val EXTRA_LOGGED_OUT = \"extra_logged_out\"\n    }\n\n    private val viewModel: LoginViewModel by viewModel()\n\n    private lateinit var binding: ActivityAuthenticationBinding\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ActivityAuthenticationBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        if (intent.getBooleanExtra(EXTRA_LOGGED_OUT, false)) {\n            showWasLoggedOutInfo()\n        }\n\n        val username = binding.fieldUsername\n        val password = binding.fieldPassword\n        val login = binding.btnLogin\n\n        viewModel.loginFormState.observe(this) { loginFormState ->\n            val loginState = loginFormState ?: return@observe\n\n            // disable login button unless both username / password is valid\n            login.isEnabled = loginState.isDataValid\n\n            username.error = loginState.usernameError\n                ?.takeIf { username.editText?.isFocused == false }\n                ?.let { getString(it) }\n        }\n\n        viewModel.loginResult.observe(this) {\n            val loginResult = it ?: return@observe\n\n            if (loginResult.error != null) {\n                showLoginFailed(loginResult.error)\n            }\n            if (loginResult.success != null) {\n                updateUiWithUser(loginResult.success)\n\n                setResult(Activity.RESULT_OK)\n                finish()\n            }\n        }\n\n        viewModel.isLoggingIn.observe(this) {\n            binding.layoutLoading.isVisible = it\n        }\n\n        username.afterTextChanged {\n            viewModel.loginDataChanged(\n                username.text(),\n                password.text()\n            )\n        }\n\n        password.apply {\n            afterTextChanged {\n                viewModel.loginDataChanged(\n                    username.text(),\n                    password.text()\n                )\n            }\n\n            editText?.setOnEditorActionListener { _, actionId, _ ->\n                when (actionId) {\n                    EditorInfo.IME_ACTION_DONE ->\n                        viewModel.login(\n                            username.text(),\n                            password.text()\n                        )\n                }\n                false\n            }\n        }\n\n        login.setOnClickListener {\n            viewModel.login(username.text(), password.text())\n        }\n\n        binding.apply {\n            tvCreateAccount.movementMethod = LinkMovementMethod.getInstance()\n\n            toolbar.initWindowInsetsListener(false)\n            toolbar.setNavigationOnClickListener {\n                setResult(Activity.RESULT_CANCELED)\n                finish()\n            }\n\n            nsvContent.initPaddingWindowInsetsListener(left = true, right = true, bottom = true, consume = false)\n            root.initImePaddingWindowInsetsListener()\n        }\n    }\n\n    private fun updateUiWithUser(model: LoggedInUserView) {\n        val displayName = model.displayName\n        Toast.makeText(\n            applicationContext,\n            getString(R.string.logged_in_success, displayName),\n            Toast.LENGTH_LONG\n        ).show()\n    }\n\n    private fun showLoginFailed(@StringRes errorString: Int) {\n        binding.fieldPassword.error = getString(errorString)\n        Toast.makeText(\n            applicationContext,\n            errorString,\n            Toast.LENGTH_SHORT\n        ).show()\n    }\n\n    private fun showWasLoggedOutInfo() {\n        binding.tvAdditionalInfo.apply {\n            setText(R.string.info_logged_out_token_expired)\n            isVisible = true\n        }\n    }\n\n}\n\n/**\n * Extension function to simplify setting an afterTextChanged action to EditText components.\n */\nfun TextInputLayout.afterTextChanged(afterTextChanged: (String) -> Unit) {\n    this.editText?.addTextChangedListener(object : TextWatcher {\n        override fun afterTextChanged(editable: Editable?) {\n            afterTextChanged.invoke(editable.toString())\n        }\n\n        override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}\n\n        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}\n    })\n}\n\nfun TextInputLayout.text(): String = this.editText!!.text.toString()"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/authentication/LoggedInUserView.kt",
    "content": "package io.github.drumber.kitsune.ui.authentication\n\n/**\n * User details post authentication that is exposed to the UI\n */\ndata class LoggedInUserView(\n    val displayName: String\n    //... other data fields that may be accessible to the UI\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/authentication/LoginFormState.kt",
    "content": "package io.github.drumber.kitsune.ui.authentication\n\n/**\n * Data validation state of the login form.\n */\ndata class LoginFormState(\n    val usernameError: Int? = null,\n    val isDataValid: Boolean = false\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/authentication/LoginResultUi.kt",
    "content": "package io.github.drumber.kitsune.ui.authentication\n\n/**\n * Authentication result : success (user details) or error message.\n */\ndata class LoginResultUi(\n    val success: LoggedInUserView? = null,\n    val error: Int? = null\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/authentication/LoginViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.authentication\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.domain.auth.LogInUserUseCase\nimport io.github.drumber.kitsune.domain.auth.LoginResult\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nclass LoginViewModel(private val logInUser: LogInUserUseCase) : ViewModel() {\n\n    private val _loginForm = MutableLiveData<LoginFormState>()\n    val loginFormState: LiveData<LoginFormState> = _loginForm\n\n    private val _loginResult = MutableLiveData<LoginResultUi>()\n    val loginResult: LiveData<LoginResultUi> = _loginResult\n\n    private val _isLoggingIn = MutableLiveData<Boolean>()\n    val isLoggingIn: LiveData<Boolean> = _isLoggingIn\n\n    fun login(username: String, password: String) {\n        _isLoggingIn.value = true\n        viewModelScope.launch(Dispatchers.IO) {\n            val result = logInUser(username, password)\n\n            if(result is LoginResult.Error) {\n                logE(\"Failed to login to Kitsu.\", result.exception)\n            }\n\n            withContext(Dispatchers.Main) {\n                if (result is LoginResult.Success) {\n                    _loginResult.value =\n                        LoginResultUi(success = LoggedInUserView(displayName = result.localUser?.name ?: \"Unknown\"))\n\n                } else {\n                    _loginResult.value = LoginResultUi(error = R.string.login_failed)\n                }\n                _isLoggingIn.value = false\n            }\n        }\n    }\n\n    fun loginDataChanged(username: String, password: String) {\n        if (!isUserNameValid(username)) {\n            _loginForm.value = LoginFormState(usernameError = R.string.invalid_username)\n        } else if (isPasswordValid(password)) {\n            _loginForm.value = LoginFormState(isDataValid = true)\n        }\n    }\n\n    private fun isUserNameValid(username: String): Boolean {\n        // verify that username is an email address\n        return (\"\"\"^\\S+@\\S+\\.\\S+$\"\"\".toRegex().matches(username))\n    }\n\n    private fun isPasswordValid(password: String): Boolean {\n        return password.isNotBlank()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/base/BaseActivity.kt",
    "content": "package io.github.drumber.kitsune.ui.base\n\nimport android.app.ActivityManager\nimport android.content.Context\nimport android.graphics.BitmapFactory\nimport android.os.Build\nimport android.os.Bundle\nimport android.view.Window\nimport androidx.annotation.StyleRes\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport androidx.core.view.WindowCompat\nimport com.chibatching.kotpref.livedata.asLiveData\nimport com.google.android.material.color.DynamicColors\nimport com.google.android.material.elevation.SurfaceColors\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.util.extensions.setStatusBarColorRes\n\nabstract class BaseActivity(\n    private val updateSystemUiColors: Boolean = true,\n    private val setAppTheme: Boolean = true\n) : AppCompatActivity() {\n\n    @StyleRes\n    private var appliedThemeRes: Int = -1\n    private var isDynamicColorsApplied = false\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)\n        if (setAppTheme) {\n            // apply app theme\n            appliedThemeRes = getThemeResFromPreference()\n            setTheme(appliedThemeRes)\n            if (DynamicColors.isDynamicColorAvailable() && KitsunePref.useDynamicColorTheme) {\n                DynamicColors.applyToActivityIfAvailable(this)\n                isDynamicColorsApplied = true\n            }\n        }\n\n        super.onCreate(savedInstanceState)\n\n        KitsunePref.asLiveData(KitsunePref::appTheme).observe(this) {\n            checkAppTheme()\n        }\n        KitsunePref.asLiveData(KitsunePref::oledBlackMode).observe(this) {\n            checkAppTheme()\n        }\n        KitsunePref.asLiveData(KitsunePref::useDynamicColorTheme).observe(this) {\n            checkAppTheme()\n        }\n\n        // set app bar color in recent apps overview\n        setAppTaskColor(SurfaceColors.SURFACE_0.getColor(this))\n\n        initEdgeToEdge()\n    }\n\n    override fun onResume() {\n        super.onResume()\n        checkAppTheme()\n    }\n\n    private fun checkAppTheme() {\n        if (!setAppTheme) return\n        if (appliedThemeRes != getThemeResFromPreference()\n            || isDynamicColorsApplied != KitsunePref.useDynamicColorTheme\n        ) {\n            recreate()\n        }\n    }\n\n    private fun getThemeResFromPreference(): Int {\n        val appTheme = KitsunePref.appTheme\n        return if (KitsunePref.oledBlackMode)\n            appTheme.blackThemeRes\n        else\n            appTheme.themeRes\n    }\n\n    private fun initEdgeToEdge() {\n        WindowCompat.setDecorFitsSystemWindows(window, false)\n        if (!updateSystemUiColors) return\n\n        setStatusBarColorRes(android.R.color.transparent)\n        if (Build.VERSION.SDK_INT >= 27) {\n            window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)\n        }\n    }\n\n    /**\n     * Change the color of the app bar that is visible in the recent\n     * app overview. This does only change the color for the current\n     * activity task.\n     * @param color     the new color that should be applied\n     */\n    private fun setAppTaskColor(color: Int) {\n        val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager\n        // find the app task that corresponds to this activity\n        val appTask = activityManager.appTasks.firstOrNull {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                it.taskInfo.taskId == taskId\n            } else {\n                it.taskInfo.id == taskId\n            }\n        }\n        // change the color of the task description, but keep the label and app icon\n        appTask?.taskInfo?.taskDescription?.let {\n            val taskDescription = when {\n                Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> ActivityManager.TaskDescription.Builder()\n                    .setPrimaryColor(color)\n                    .build()\n\n                Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> ActivityManager.TaskDescription(\n                    it.label,\n                    R.mipmap.ic_launcher,\n                    color\n                )\n\n                else -> ActivityManager.TaskDescription(\n                    it.label,\n                    BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher),\n                    color\n                )\n            }\n            setTaskDescription(taskDescription)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/base/BaseDialogFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.base\n\nimport android.annotation.SuppressLint\nimport android.graphics.Color\nimport android.util.TypedValue\nimport android.view.ViewGroup\nimport android.view.WindowManager\nimport androidx.annotation.LayoutRes\nimport androidx.appcompat.app.AppCompatDialogFragment\nimport androidx.core.view.WindowCompat\nimport com.google.android.material.color.DynamicColors\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.preference.KitsunePref\n\nabstract class BaseDialogFragment(\n    @LayoutRes layoutRes: Int,\n    private val isEdgeToEdge: Boolean = true\n) : AppCompatDialogFragment(layoutRes) {\n\n    @SuppressLint(\"RestrictedApi\")\n    override fun onStart() {\n        super.onStart()\n        dialog?.window?.apply {\n            setLayout(\n                ViewGroup.LayoutParams.MATCH_PARENT,\n                ViewGroup.LayoutParams.MATCH_PARENT\n            )\n            addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)\n            attributes?.dimAmount = 0.8f\n            setWindowAnimations(R.style.Theme_Kitsune_Slide)\n\n            if (isEdgeToEdge) {\n                WindowCompat.setDecorFitsSystemWindows(this, false)\n                statusBarColor = Color.TRANSPARENT\n                navigationBarColor = Color.TRANSPARENT\n            }\n        }\n    }\n\n    override fun getTheme(): Int {\n        if (KitsunePref.useDynamicColorTheme && DynamicColors.isDynamicColorAvailable())\n            return R.style.Theme_Kitsune_FullScreenDialog_Dynamic\n        val typedValue = TypedValue()\n        requireActivity().theme.resolveAttribute(R.attr.fullScreenDialogTheme, typedValue, true)\n        return typedValue.data\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/base/BaseFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.base\n\nimport androidx.annotation.LayoutRes\nimport androidx.fragment.app.Fragment\nimport io.github.drumber.kitsune.ui.main.FragmentDecorationPreference\n\nabstract class BaseFragment(\n    @LayoutRes contentLayoutId: Int,\n    override val hasTransparentStatusBar: Boolean = true\n) : Fragment(contentLayoutId), FragmentDecorationPreference\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/base/BasePreferenceFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.base\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.inputmethod.InputMethodManager\nimport androidx.annotation.StringRes\nimport androidx.navigation.fragment.findNavController\nimport androidx.preference.EditTextPreference\nimport androidx.preference.ListPreference\nimport androidx.preference.MultiSelectListPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceFragmentCompat\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.CustomEditTextPreferenceBinding\nimport io.github.drumber.kitsune.databinding.FragmentPreferenceBinding\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\n\nabstract class BasePreferenceFragment(\n    @StringRes private val title: Int = R.string.nav_settings\n) : PreferenceFragmentCompat() {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val binding = FragmentPreferenceBinding.bind(view)\n        binding.apply {\n            collapsingToolbar.initWindowInsetsListener(consume = false)\n            toolbar.initWindowInsetsListener(consume = false)\n            toolbar.setTitle(title)\n            toolbar.setNavigationOnClickListener { findNavController().navigateUp() }\n        }\n    }\n\n    override fun onCreateRecyclerView(\n        inflater: LayoutInflater,\n        parent: ViewGroup,\n        savedInstanceState: Bundle?\n    ): RecyclerView {\n        val recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState)\n        recyclerView.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            bottom = true,\n            consume = false\n        )\n        return recyclerView\n    }\n\n    override fun onDisplayPreferenceDialog(preference: Preference) {\n        // replace old AlertDialog from PreferenceFragmentCompat with new MaterialAlertDialog\n        val builder = MaterialAlertDialogBuilder(requireContext())\n            .setTitle(preference.title)\n            .setIcon(preference.icon)\n            .setNegativeButton(android.R.string.cancel, null)\n\n        when (preference) {\n            is EditTextPreference -> {\n                val binding = CustomEditTextPreferenceBinding.inflate(layoutInflater)\n                binding.textInputLayout.editText?.apply {\n                    setText(preference.text)\n                    requestFocus()\n                    post {\n                        if (!isAdded) return@post\n                        val imm = requireContext()\n                            .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager\n                        imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)\n                    }\n                }\n\n                builder\n                    .setView(binding.root)\n                    .setPositiveButton(android.R.string.ok) { dialog, _ ->\n                        val inputText = binding.textInputLayout.editText?.text?.toString()\n                        if (preference.callChangeListener(inputText)) {\n                            preference.text = inputText\n                        }\n                        dialog.dismiss()\n                    }\n            }\n\n            is ListPreference -> {\n                val selectedIndex = preference.findIndexOfValue(preference.value)\n\n                builder.setSingleChoiceItems(preference.entries, selectedIndex) { dialog, index ->\n                    val value = preference.entryValues[index].toString()\n                    if (preference.callChangeListener(value)) {\n                        preference.value = value\n                    }\n                    dialog.dismiss()\n                }\n            }\n\n            is MultiSelectListPreference -> {\n                val newValues = preference.values\n                val checkedItems = preference.entryValues.map { entryValue ->\n                    newValues.contains(entryValue)\n                }.toBooleanArray()\n\n                builder\n                    .setMultiChoiceItems(\n                        preference.entries,\n                        checkedItems\n                    ) { _, index, isChecked ->\n                        val value = preference.entryValues[index].toString()\n                        if (isChecked) {\n                            newValues.add(value)\n                        } else {\n                            newValues.remove(value)\n                        }\n                    }\n                    .setPositiveButton(android.R.string.ok) { dialog, _ ->\n                        if (preference.callChangeListener(newValues)) {\n                            preference.values = newValues\n                        }\n                    }\n            }\n        }\n\n        builder.show()\n    }\n\n    protected fun <T : Preference> findPreference(@StringRes preferenceKey: Int): T? {\n        return findPreference(getString(preferenceKey))\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/CustomNumberSpinner.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.widget.LinearLayout\nimport androidx.appcompat.widget.TooltipCompat\nimport androidx.core.view.isVisible\nimport androidx.core.widget.doAfterTextChanged\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.CustomNumberSpinnerBinding\n\ntypealias ValueChangedListener = (Int) -> Unit\n\nclass CustomNumberSpinner @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :\n    LinearLayout(context, attrs) {\n\n    private val binding: CustomNumberSpinnerBinding\n\n    private var maxValue: Int = Int.MAX_VALUE\n    private var minValue: Int = 0\n\n    private var suffixMode = SuffixMode.Disabled\n    private var customSuffixText: String? = null\n\n    private var onValueChangedListener: ValueChangedListener? = null\n\n    private var ignoreTextChangedEvent: Boolean = false\n\n    init {\n        val view = inflate(context, R.layout.custom_number_spinner, this)\n        binding = CustomNumberSpinnerBinding.bind(view)\n\n        attrs?.let {\n            val a = context.obtainStyledAttributes(it, R.styleable.CustomNumberSpinner)\n            binding.apply {\n                layoutAction.isVisible =\n                    a.getBoolean(R.styleable.CustomNumberSpinner_enableAction, false)\n                btnAction.text = a.getString(R.styleable.CustomNumberSpinner_actionText)\n                setActionTooltip(a.getString(R.styleable.CustomNumberSpinner_actionTooltip))\n\n                fieldCount.setText(a.getInt(R.styleable.CustomNumberSpinner_value, 0).toString())\n                maxValue = a.getInt(R.styleable.CustomNumberSpinner_maxValue, Int.MAX_VALUE)\n                minValue = a.getInt(R.styleable.CustomNumberSpinner_minValue, 0)\n\n                suffixMode =\n                    SuffixMode.entries[a.getInt(R.styleable.CustomNumberSpinner_suffixMode, 0)]\n                tvSuffix.isVisible = suffixMode != SuffixMode.Disabled\n                customSuffixText = a.getString(R.styleable.CustomNumberSpinner_suffixText)\n                updateSuffixText()\n            }\n            a.recycle()\n        }\n\n        binding.apply {\n            fieldCount.doAfterTextChanged {\n                if (ignoreTextChangedEvent) {\n                    return@doAfterTextChanged\n                }\n\n                val value = it?.toString()?.toIntOrNull()?.coerceIn(minValue..maxValue) ?: minValue\n                onValueChangedListener?.invoke(value)\n            }\n\n            btnDecrement.setOnClickListener {\n                val value = getValue()?.minus(1) ?: minValue\n                setValue(value)\n                onValueChangedListener?.invoke(value)\n            }\n            btnIncrement.setOnClickListener {\n                val value = (getValue() ?: minValue).plus(1)\n                setValue(value)\n                onValueChangedListener?.invoke(value)\n            }\n        }\n    }\n\n    private fun updateButtonState() {\n        val value = getValue() ?: minValue\n        binding.apply {\n            btnDecrement.isEnabled = value > minValue\n            btnIncrement.isEnabled = value < maxValue\n        }\n    }\n\n    private fun updateSuffixText() {\n        binding.tvSuffix.text = if (suffixMode == SuffixMode.MaxValue) {\n            \"/ $maxValue\"\n        } else {\n            customSuffixText\n        }\n    }\n\n    fun setValue(value: Int) {\n        ignoreTextChangedEvent = true\n        binding.fieldCount.apply {\n            editableText.clear()\n            append(value.coerceIn(minValue..maxValue).toString())\n        }\n        ignoreTextChangedEvent = false\n        updateButtonState()\n    }\n\n    fun getValue() = binding.fieldCount.text?.toString()?.toIntOrNull()\n\n    fun setValueChangedListener(onValueChanged: ValueChangedListener?) {\n        onValueChangedListener = onValueChanged\n    }\n\n    fun setMaxValue(maxValue: Int) {\n        this.maxValue = maxValue\n        // make sure value is within new range\n        getValue()?.let { setValue(it) }\n        if (suffixMode == SuffixMode.MaxValue) {\n            updateSuffixText()\n        }\n    }\n\n    fun getMaxValue() = maxValue\n\n    fun setMinValue(minValue: Int) {\n        this.minValue = minValue\n        // make sure value is within new range\n        getValue()?.let { setValue(it) }\n    }\n\n    fun getMinValue() = minValue\n\n    fun setSuffixMode(suffixMode: SuffixMode) {\n        this.suffixMode = suffixMode\n        binding.tvSuffix.isVisible = this.suffixMode != SuffixMode.Disabled\n        updateSuffixText()\n    }\n\n    fun getSuffixModel() = suffixMode\n\n    fun setSuffixCustomText(suffixText: String) {\n        customSuffixText = suffixText\n        if (suffixMode == SuffixMode.CustomText) {\n            updateSuffixText()\n        }\n    }\n\n    fun setActionEnabled(enableAction: Boolean) {\n        binding.layoutAction.isVisible = enableAction\n    }\n\n    fun isActionEnabled() = binding.layoutAction.isVisible\n\n    fun setActionText(actionText: String) {\n        binding.btnAction.text = actionText\n    }\n\n    fun setActionTooltip(tooltip: String?) {\n        TooltipCompat.setTooltipText(binding.btnAction, tooltip)\n    }\n\n    fun setActionClickListener(l: OnClickListener?) {\n        binding.btnAction.setOnClickListener(l)\n    }\n\n    enum class SuffixMode {\n        Disabled, MaxValue, CustomText\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/ExpandableLayout.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport android.animation.ValueAnimator\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.widget.FrameLayout\nimport androidx.core.animation.addListener\nimport androidx.core.os.bundleOf\nimport androidx.interpolator.view.animation.FastOutSlowInInterpolator\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.ui.component.ExpandableLayout.State.COLLAPSED\nimport io.github.drumber.kitsune.ui.component.ExpandableLayout.State.COLLAPSING\nimport io.github.drumber.kitsune.ui.component.ExpandableLayout.State.EXPANDED\nimport io.github.drumber.kitsune.ui.component.ExpandableLayout.State.EXPANDING\nimport kotlin.math.round\n\n/**\n * Layout for expanding and collapsing the views height with an optional minimum height.\n *\n * Modified version of https://github.com/cachapa/ExpandableLayout\n */\nclass ExpandableLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :\n    FrameLayout(context, attrs) {\n\n    enum class State {\n        COLLAPSED,\n        COLLAPSING,\n        EXPANDING,\n        EXPANDED\n    }\n\n    companion object {\n        const val KEY_SUPER_STATE = \"super_state\"\n        const val KEY_EXPANSION = \"expansion\"\n    }\n\n    var duration = 300\n\n    private var expansion = 1f\n\n    var minHeight = 0f\n        set(value) {\n            field = value.coerceAtLeast(0f)\n        }\n\n    private var state = EXPANDED\n        set(value) {\n            field = value\n            if (_expandedState.value != isExpanded()) {\n                _expandedState.postValue(isExpanded())\n            }\n        }\n\n    private val _expandedState = MutableLiveData(isExpanded())\n    val expandedState get() = _expandedState as LiveData<Boolean>\n\n    var interpolator = FastOutSlowInInterpolator()\n    private var animator: ValueAnimator? = null\n\n    init {\n        attrs?.let {\n            val a = context.obtainStyledAttributes(it, R.styleable.ExpandableLayout)\n            duration = a.getInt(R.styleable.ExpandableLayout_duration, duration)\n            expansion = if (a.getBoolean(R.styleable.ExpandableLayout_expanded, true)) 1f else 0f\n            minHeight = a.getDimension(R.styleable.ExpandableLayout_min_height, minHeight)\n            a.recycle()\n\n            state = if (expansion == 0f && minHeight > 0f) COLLAPSED else EXPANDED\n        }\n    }\n\n    override fun onSaveInstanceState(): Parcelable {\n        val superState = super.onSaveInstanceState()\n        expansion = if (isExpanded()) 1f else 0f\n        return bundleOf(\n            KEY_EXPANSION to expansion,\n            KEY_SUPER_STATE to superState\n        )\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable?) {\n        if (state == null) {\n            return super.onRestoreInstanceState(null)\n        }\n\n        val bundle = state as Bundle\n        expansion = bundle.getFloat(KEY_EXPANSION)\n        this.state = if (expansion == 1f) EXPANDED else COLLAPSED\n        val superState = bundle.getParcelable<Parcelable>(KEY_SUPER_STATE)\n\n        super.onRestoreInstanceState(superState)\n    }\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec)\n\n        val width = measuredWidth\n        val height = measuredHeight\n\n        val minHeight = minHeight.coerceAtMost(height.toFloat())\n        val calculatedHeight = (minHeight + round((height - minHeight) * expansion)).toInt()\n        setMeasuredDimension(width, calculatedHeight)\n    }\n\n    override fun onConfigurationChanged(newConfig: Configuration?) {\n        animator?.cancel()\n        super.onConfigurationChanged(newConfig)\n    }\n\n    fun isExpanded() = state == EXPANDING || state == EXPANDED\n\n    @JvmOverloads\n    fun toggle(animate: Boolean = true) {\n        if (isExpanded()) {\n            collapse(animate)\n        } else {\n            expand(animate)\n        }\n    }\n\n    fun expand(animate: Boolean = true) {\n        setExpanded(true, animate)\n    }\n\n    fun collapse(animate: Boolean = true) {\n        setExpanded(false, animate)\n    }\n\n    fun setExpanded(expand: Boolean, animate: Boolean = true) {\n        if (expand == isExpanded()) return\n\n        val targetExpansion = if (expand) 1f else 0f\n        if (animate) {\n            animateSize(targetExpansion)\n        } else {\n            setExpansion(targetExpansion)\n        }\n    }\n\n    fun getExpansion() = expansion\n\n    fun setExpansion(expansion: Float) {\n        if (this.expansion == expansion) return\n\n        val delta = expansion - this.expansion\n        state = when {\n            expansion == 0f -> COLLAPSED\n            expansion == 1f -> EXPANDED\n            delta < 0 -> COLLAPSING\n            else -> EXPANDING\n        }\n\n        this.expansion = expansion\n        requestLayout()\n    }\n\n    private fun animateSize(targetExpansion: Float) {\n        animator?.cancel()\n        animator = ValueAnimator.ofFloat(expansion, targetExpansion).apply {\n            interpolator = this@ExpandableLayout.interpolator\n            duration = this@ExpandableLayout.duration.toLong()\n\n            addUpdateListener {\n                setExpansion(it.animatedValue as Float)\n            }\n\n            var canceled = false\n            addListener(\n                onStart = {\n                    state = if (targetExpansion == 0f) COLLAPSING else EXPANDING\n                },\n                onEnd = {\n                    if (!canceled) {\n                        state = if (targetExpansion == 0f) COLLAPSED else EXPANDED\n                    }\n                },\n                onCancel = { canceled = true }\n            )\n\n            start()\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/ExploreSection.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport android.content.Context\nimport android.view.View\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.RequestManager\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.databinding.SectionMainExploreBinding\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.adapter.MediaRecyclerViewAdapter\nimport io.github.drumber.kitsune.ui.adapter.OnItemClickListener\nimport java.util.concurrent.CopyOnWriteArrayList\n\nclass ExploreSection(\n    private val glide: RequestManager,\n    private val title: String,\n    private val initialData: List<Media>? = null,\n    private val itemListener: OnItemClickListener<Media>? = null,\n    private val headerListener: OnHeaderClickListener? = null\n) {\n\n    private lateinit var exploreAdapter: MediaRecyclerViewAdapter\n\n    fun bindView(view: View) {\n        val binding = SectionMainExploreBinding.bind(view)\n        initView(view.context, binding)\n    }\n\n    private fun initView(context: Context, binding: SectionMainExploreBinding) {\n        exploreAdapter = MediaRecyclerViewAdapter(\n            if(initialData != null) CopyOnWriteArrayList(initialData) else CopyOnWriteArrayList(),\n            glide,\n            itemSize = KitsunePref.mediaItemSize,\n            transitionNameSuffix = \"_$title\",\n            listener = itemListener\n        )\n\n        binding.apply {\n            rvMedia.apply {\n                layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)\n                adapter = exploreAdapter\n            }\n\n            tvTitle.text = title\n\n            header.setOnClickListener {\n                headerListener?.onHeaderClick()\n            }\n        }\n    }\n\n    fun setData(dataSet: List<Media>) {\n        exploreAdapter.dataSet.apply {\n            clear()\n            addAll(dataSet)\n        }\n        exploreAdapter.notifyDataSetChanged()\n    }\n\n    fun interface OnHeaderClickListener {\n        fun onHeaderClick()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/LayoutResourceLoadingLoadState.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport androidx.core.view.isVisible\nimport androidx.paging.CombinedLoadStates\nimport androidx.paging.LoadState\nimport androidx.recyclerview.widget.RecyclerView\nimport io.github.drumber.kitsune.databinding.LayoutResourceLoadingBinding\n\nfun LayoutResourceLoadingBinding.updateLoadState(\n    recyclerView: RecyclerView,\n    itemCount: Int,\n    state: CombinedLoadStates,\n    useRemoteMediator: Boolean = false,\n    checkIsNotLoading: () -> Boolean = { state.refresh is LoadState.NotLoading }\n) {\n    val remoteState = if (useRemoteMediator) state.mediator else state.source\n\n    val isNotLoading = checkIsNotLoading()\n    recyclerView.isVisible = isNotLoading\n    root.isVisible = !isNotLoading\n    progressBar.isVisible = state.refresh is LoadState.Loading\n    btnRetry.isVisible = remoteState?.refresh is LoadState.Error\n    tvError.isVisible = remoteState?.refresh is LoadState.Error\n\n    if (state.refresh is LoadState.NotLoading\n        && state.append.endOfPaginationReached\n        && itemCount < 1\n    ) {\n        root.isVisible = true\n        tvNoData.isVisible = true\n        recyclerView.isVisible = false\n    } else {\n        tvNoData.isVisible = false\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/LoadStateSpanSizeLookup.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport androidx.paging.LoadState\nimport androidx.paging.PagingDataAdapter\nimport androidx.recyclerview.widget.GridLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\n\nclass LoadStateSpanSizeLookup<T : Any, VH : RecyclerView.ViewHolder>(\n    private val adapter: PagingDataAdapter<T, VH>,\n    private val gridLayoutManager: GridLayoutManager\n) : GridLayoutManager.SpanSizeLookup() {\n\n    private var append: LoadState? = null\n    private var prepend: LoadState? = null\n\n    init {\n        adapter.addLoadStateListener {\n            append = it.append\n            prepend = it.prepend\n        }\n    }\n\n    override fun getSpanSize(position: Int): Int {\n        if(position == 0 && prepend !is LoadState.NotLoading) {\n            return gridLayoutManager.spanCount\n        }\n        val totalCount = adapter.itemCount.plus(if(prepend !is LoadState.NotLoading) 1 else 0)\n        if(position >= totalCount && append !is LoadState.NotLoading) {\n            return gridLayoutManager.spanCount\n        }\n        return 1\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/MediaItemCard.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.ViewGroup\nimport com.google.android.material.card.MaterialCardView\nimport io.github.drumber.kitsune.constants.MediaItemSize\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport kotlin.math.roundToInt\n\nclass MediaItemCard @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null\n) : MaterialCardView(context, attrs) {\n\n    var isInGridLayout = false\n\n    /** Used size if [isInGridLayout] is false */\n    private var customItemSize = MediaItemSize.SMALL\n\n    fun setCustomItemSize(customItemSize: MediaItemSize) {\n        this.customItemSize = customItemSize\n    }\n\n    override fun getLayoutParams(): ViewGroup.LayoutParams {\n        return super.getLayoutParams().apply {\n            if (!isInGridLayout) {\n                width = resources.getDimensionPixelSize(customItemSize.widthRes)\n                height = resources.getDimensionPixelSize(customItemSize.heightRes)\n            }\n        }\n    }\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        if (!isInGridLayout) {\n            super.onMeasure(widthMeasureSpec, heightMeasureSpec)\n            return\n        }\n\n        val width = MeasureSpec.getSize(widthMeasureSpec)\n\n        // original size from preference to calculate the height in the correct aspect ratio\n        val origWidth = resources.getDimensionPixelSize(KitsunePref.mediaItemSize.widthRes)\n        val origHeight = resources.getDimensionPixelSize(KitsunePref.mediaItemSize.heightRes)\n\n        val newHeight = ((origHeight.toFloat() / origWidth.toFloat()) * width).roundToInt()\n        val newHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)\n        super.onMeasure(widthMeasureSpec, newHeightSpec)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/NestedScrollableHost.kt",
    "content": "// ViewPager2 nested scroll helper class copied from:\n// https://github.com/android/views-widgets-samples/blob/f22069a57d5df0b58ce0be08086c3e9db35870b8/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/NestedScrollableHost.kt\n/*\n * Copyright 2019 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 io.github.drumber.kitsune.ui.component\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewConfiguration\nimport android.widget.FrameLayout\nimport androidx.viewpager2.widget.ViewPager2\nimport androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL\nimport kotlin.math.absoluteValue\nimport kotlin.math.sign\n\n/**\n * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem\n * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as\n * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.\n *\n * This solution has limitations when using multiple levels of nested scrollable elements\n * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).\n */\nclass NestedScrollableHost : FrameLayout {\n    constructor(context: Context) : super(context)\n    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)\n\n    private var touchSlop = 0\n    private var initialX = 0f\n    private var initialY = 0f\n    private val parentViewPager: ViewPager2?\n        get() {\n            var v: View? = parent as? View\n            while (v != null && v !is ViewPager2) {\n                v = v.parent as? View\n            }\n            return v as? ViewPager2\n        }\n\n    private val child: View? get() = if (childCount > 0) getChildAt(0) else null\n\n    init {\n        touchSlop = ViewConfiguration.get(context).scaledTouchSlop\n    }\n\n    private fun canChildScroll(orientation: Int, delta: Float): Boolean {\n        val direction = -delta.sign.toInt()\n        return when (orientation) {\n            0 -> child?.canScrollHorizontally(direction) ?: false\n            1 -> child?.canScrollVertically(direction) ?: false\n            else -> throw IllegalArgumentException()\n        }\n    }\n\n    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {\n        handleInterceptTouchEvent(e)\n        return super.onInterceptTouchEvent(e)\n    }\n\n    private fun handleInterceptTouchEvent(e: MotionEvent) {\n        val orientation = parentViewPager?.orientation ?: return\n\n        // Early return if child can't scroll in same direction as parent\n        if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {\n            return\n        }\n\n        if (e.action == MotionEvent.ACTION_DOWN) {\n            initialX = e.x\n            initialY = e.y\n            parent.requestDisallowInterceptTouchEvent(true)\n        } else if (e.action == MotionEvent.ACTION_MOVE) {\n            val dx = e.x - initialX\n            val dy = e.y - initialY\n            val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL\n\n            // assuming ViewPager2 touch-slop is 2x touch-slop of child\n            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f\n            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f\n\n            if (scaledDx > touchSlop || scaledDy > touchSlop) {\n                if (isVpHorizontal == (scaledDy > scaledDx)) {\n                    // Gesture is perpendicular, allow all parents to intercept\n                    parent.requestDisallowInterceptTouchEvent(false)\n                } else {\n                    // Gesture is parallel, query child if movement in that direction is possible\n                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {\n                        // Child can scroll, disallow all parents to intercept\n                        parent.requestDisallowInterceptTouchEvent(true)\n                    } else {\n                        // Child cannot scroll, allow all parents to intercept\n                        parent.requestDisallowInterceptTouchEvent(false)\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/PhotoViewNestedScrollView.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.MotionEvent\nimport androidx.core.widget.NestedScrollView\n\n/**\n * Sets the size of its children equal to its own size and manages scroll interceptions.\n */\nclass PhotoViewNestedScrollView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0\n) : NestedScrollView(context, attrs, defStyleAttr) {\n\n    companion object {\n        private const val SCROLL_UP_THRESHOLD = 60f\n    }\n\n    private var disallowInterceptTouchEvents = false\n\n    private var startY = -1f\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec)\n\n        if (!isFillViewport) {\n            return\n        }\n\n        val heightMode = MeasureSpec.getMode(heightMeasureSpec)\n        if (heightMode == MeasureSpec.UNSPECIFIED) {\n            return\n        }\n\n        if (childCount > 0) {\n            val child = getChildAt(0)\n            child.measure(widthMeasureSpec, heightMeasureSpec)\n        }\n    }\n\n    override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {\n        super.requestDisallowInterceptTouchEvent(disallowIntercept)\n        disallowInterceptTouchEvents = disallowIntercept\n    }\n\n    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {\n        if (ev.action == MotionEvent.ACTION_DOWN) {\n            startY = ev.y\n        }\n        val isScrollingUp = (startY != -1f && ev.y - startY > SCROLL_UP_THRESHOLD)\n                || ev.action == MotionEvent.ACTION_DOWN // Hauler needs to receive the ACTION_DOWN event\n        if (ev.action == MotionEvent.ACTION_UP) {\n            startY = -1f\n        }\n\n        return !disallowInterceptTouchEvents\n                && ev.pointerCount == 1 // prevent intercepting pinch-to-zoom gesture\n                && isScrollingUp // only intercept scroll-up actions\n                && super.onInterceptTouchEvent(ev)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/ResponsiveGridLayoutManager.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport android.content.Context\nimport androidx.recyclerview.widget.GridLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport kotlin.math.max\n\nclass ResponsiveGridLayoutManager(\n    context: Context,\n    private val columnWidth: Int,\n    private val minColumns: Int = 1,\n    @RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL,\n    reverseLayout: Boolean = false\n) : GridLayoutManager(context, 1, orientation, reverseLayout) {\n\n    override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {\n        val availableWidth = width - paddingRight - paddingLeft\n        spanCount = max(minColumns, availableWidth / columnWidth)\n        super.onLayoutChildren(recycler, state)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/UniqueStateRecyclerView.kt",
    "content": "package io.github.drumber.kitsune.ui.component\n\nimport android.content.Context\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.util.SparseArray\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\n\n/**\n * RecyclerView that uses a unique ID to save and restore its state.\n */\nclass UniqueStateRecyclerView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = androidx.recyclerview.R.attr.recyclerViewStyle\n) : RecyclerView(context, attrs, defStyleAttr) {\n\n    var uniqueId: Int = View.NO_ID\n\n    override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {\n        if (uniqueId == View.NO_ID) {\n            return super.dispatchSaveInstanceState(container)\n        }\n\n        val stateId = uniqueId xor id\n        val state = onSaveInstanceState()\n        if (state != null) {\n            container.put(stateId, state)\n        }\n    }\n\n    override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable?>?) {\n        if (uniqueId == View.NO_ID) {\n            return super.dispatchRestoreInstanceState(container)\n        }\n\n        val stateId = uniqueId xor id\n        val state = container?.get(stateId)\n        if (state != null) {\n            onRestoreInstanceState(state)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/algolia/SeasonListPresenter.kt",
    "content": "package io.github.drumber.kitsune.ui.component.algolia\n\nimport com.algolia.instantsearch.filter.facet.FacetListItem\nimport com.algolia.instantsearch.filter.facet.FacetListPresenter\n\nclass SeasonListPresenter(\n    private val sortOrder: Array<String> = arrayOf(\"spring\", \"summer\", \"fall\", \"winter\")\n) : FacetListPresenter {\n\n    private val comparator = Comparator<FacetListItem> { (facetA, _), (facetB, _) ->\n        val indexA = sortOrder.indexOf(facetA.value.lowercase())\n        val indexB = sortOrder.indexOf(facetB.value.lowercase())\n        indexA.compareTo(indexB)\n    }\n\n    override fun invoke(selectableItems: List<FacetListItem>): List<FacetListItem> {\n        return selectableItems.sortedWith(comparator)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/algolia/range/CustomFilterRangeConnectionFilterState.kt",
    "content": "package io.github.drumber.kitsune.ui.component.algolia.range\n\nimport com.algolia.instantsearch.core.Callback\nimport com.algolia.instantsearch.core.connection.AbstractConnection\nimport com.algolia.instantsearch.core.number.range.Range\nimport com.algolia.instantsearch.filter.range.FilterRangeViewModel\nimport com.algolia.instantsearch.filter.state.FilterGroupID\nimport com.algolia.instantsearch.filter.state.FilterState\nimport com.algolia.instantsearch.filter.state.Filters\nimport com.algolia.instantsearch.filter.state.toFilterNumeric\nimport com.algolia.search.model.Attribute\nimport com.algolia.search.model.filter.Filter\n\ndata class CustomFilterRangeConnectionFilterState<T>(\n    private val viewModel: FilterRangeViewModel<T>,\n    private val filterState: FilterState,\n    private val attribute: Attribute,\n    private val groupID: FilterGroupID,\n) : AbstractConnection() where T : Number, T : Comparable<T> {\n\n    @Suppress(\"UNCHECKED_CAST\")\n    private val updateRange: Callback<Filters> = { filters ->\n        val filter = filters.getNumericFilters(groupID)\n            .filter { it.attribute == attribute }\n            .map { it.value }\n            .filterIsInstance<Filter.Numeric.Value.Range>()\n            .firstOrNull()\n\n        if (filter != null) {\n            viewModel.range.value = Range(filter.lowerBound as T, filter.upperBound as T)\n        } else {\n            // set range value to null if the filter is not set\n            viewModel.range.value = null\n        }\n    }\n\n    private val updateFilterState: Callback<Range<T>?> = { range ->\n        filterState.notify {\n            viewModel.range.value?.let { remove(groupID, it.toFilterNumeric(attribute)) }\n            if (range != null) add(groupID, range.toFilterNumeric(attribute))\n        }\n    }\n\n    override fun connect() {\n        super.connect()\n        filterState.filters.subscribePast(updateRange)\n        viewModel.eventRange.subscribe(updateFilterState)\n    }\n\n    override fun disconnect() {\n        super.disconnect()\n        filterState.filters.unsubscribe(updateRange)\n        viewModel.eventRange.unsubscribe(updateFilterState)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/algolia/range/CustomFilterRangeConnector.kt",
    "content": "package io.github.drumber.kitsune.ui.component.algolia.range\n\nimport com.algolia.instantsearch.core.connection.AbstractConnection\nimport com.algolia.instantsearch.core.connection.Connection\nimport com.algolia.instantsearch.core.number.range.Range\nimport com.algolia.instantsearch.filter.range.FilterRangeViewModel\nimport com.algolia.instantsearch.filter.state.FilterGroupID\nimport com.algolia.instantsearch.filter.state.FilterOperator\nimport com.algolia.instantsearch.filter.state.FilterState\nimport com.algolia.search.model.Attribute\n\ndata class CustomFilterRangeConnector<T>(\n    val viewModel: FilterRangeViewModel<T>,\n    val filterState: FilterState,\n    val attribute: Attribute,\n    val groupID: FilterGroupID = FilterGroupID(attribute, FilterOperator.And),\n) : AbstractConnection() where T : Number, T : Comparable<T> {\n\n    constructor(\n        filterState: FilterState,\n        attribute: Attribute,\n        bounds: ClosedRange<T>? = null,\n        range: ClosedRange<T>? = null,\n    ) : this(\n        FilterRangeViewModel(\n            range = range?.let { Range(it) },\n            bounds = bounds?.let { Range(it) }\n        ),\n        filterState, attribute\n    )\n\n    private val connectionFilterState = viewModel.connectFilterState(filterState, attribute, groupID)\n\n    override fun connect() {\n        super.connect()\n        connectionFilterState.connect()\n    }\n\n    override fun disconnect() {\n        super.disconnect()\n        connectionFilterState.disconnect()\n    }\n}\n\nfun <T> FilterRangeViewModel<T>.connectFilterState(\n    filterState: FilterState,\n    attribute: Attribute,\n    groupID: FilterGroupID = FilterGroupID(attribute, FilterOperator.And),\n): Connection where T : Number, T : Comparable<T> {\n    return CustomFilterRangeConnectionFilterState(this, filterState, attribute, groupID)\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/algolia/range/CustomNumberRangeConnectionView.kt",
    "content": "package io.github.drumber.kitsune.ui.component.algolia.range\n\nimport com.algolia.instantsearch.core.Callback\nimport com.algolia.instantsearch.core.connection.AbstractConnection\nimport com.algolia.instantsearch.core.connection.Connection\nimport com.algolia.instantsearch.core.number.range.NumberRangeViewModel\nimport com.algolia.instantsearch.core.number.range.Range\n\ndata class CustomNumberRangeConnectionView<T>(\n    private val viewModel: NumberRangeViewModel<T>,\n    private val view: CustomNumberRangeView<T>\n) : AbstractConnection() where T : Number, T : Comparable<T> {\n\n    private val updateBounds: Callback<Range<T>?> = { bounds ->\n        view.setBounds(bounds)\n    }\n    private val updateRange: Callback<Range<T>?> = { range ->\n        view.setRange(range)\n    }\n\n    override fun connect() {\n        super.connect()\n        viewModel.bounds.subscribePast(updateBounds)\n        viewModel.range.subscribePast(updateRange)\n        view.onRangeChanged = (viewModel.eventRange::send)\n    }\n\n    override fun disconnect() {\n        super.disconnect()\n        viewModel.bounds.unsubscribe(updateBounds)\n        viewModel.range.unsubscribe(updateRange)\n        view.onRangeChanged = null\n    }\n}\n\nfun <T> CustomFilterRangeConnector<T>.connectView(\n    view: CustomNumberRangeView<T>,\n): Connection where T : Number, T : Comparable<T> {\n    return CustomNumberRangeConnectionView(viewModel, view)\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/algolia/range/CustomNumberRangeView.kt",
    "content": "package io.github.drumber.kitsune.ui.component.algolia.range\n\nimport com.algolia.instantsearch.core.Callback\nimport com.algolia.instantsearch.core.number.range.Range\n\ninterface CustomNumberRangeView<T> where T : Number, T : Comparable<T> {\n\n    var onRangeChanged: Callback<Range<T>?>?\n\n    fun setRange(range: Range<T>?)\n\n    fun setBounds(bounds: Range<T>?)\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/algolia/range/IntNumberRangeView.kt",
    "content": "package io.github.drumber.kitsune.ui.component.algolia.range\n\nimport com.algolia.instantsearch.core.Callback\nimport com.algolia.instantsearch.core.number.range.Range\nimport com.google.android.material.slider.RangeSlider\n\nclass IntNumberRangeView(\n    private val slider: RangeSlider\n) : CustomNumberRangeView<Int> {\n\n    override var onRangeChanged: Callback<Range<Int>?>? = null\n\n    private var range: Range<Int>? = null\n    private var bounds: Range<Int>? = null\n\n    init {\n        slider.stepSize = 1f\n        slider.addOnSliderTouchListener(object : RangeSlider.OnSliderTouchListener {\n            override fun onStartTrackingTouch(slider: RangeSlider) {}\n\n            override fun onStopTrackingTouch(slider: RangeSlider) {\n                val valueMin = slider.values[0].toInt()\n                val valueMax = slider.values[1].toInt()\n                if (valueMin == bounds?.min && valueMax == bounds?.max) {\n                    onRangeChanged?.invoke(null)\n                } else if (range?.min != valueMin || range?.max != valueMax) {\n                    onRangeChanged?.invoke(Range(valueMin..valueMax))\n                }\n            }\n        })\n    }\n\n    override fun setRange(range: Range<Int>?) {\n        if (range == null) {\n            bounds?.let {\n                slider.setValues(it.min.toFloat(), it.max.toFloat())\n            }\n        } else if (this.range != range) {\n            slider.setValues(range.min.toFloat(), range.max.toFloat())\n        }\n        this.range = range\n    }\n\n    override fun setBounds(bounds: Range<Int>?) {\n        bounds?.let {\n            this.bounds = it\n            slider.valueFrom = it.min.toFloat()\n            slider.valueTo = it.max.toFloat()\n            slider.setValues(\n                range?.min?.toFloat() ?: it.min.toFloat(),\n                range?.max?.toFloat() ?: it.max.toFloat()\n            )\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/chart/BarChartStyle.kt",
    "content": "package io.github.drumber.kitsune.ui.component.chart\n\nimport android.content.Context\nimport com.github.mikephil.charting.charts.BarChart\nimport com.github.mikephil.charting.components.XAxis\nimport com.github.mikephil.charting.data.BarData\nimport com.github.mikephil.charting.data.BarDataSet\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.util.extensions.getColor\n\nobject BarChartStyle : BaseChartStyle() {\n\n    fun BarChart.applyStyle(\n        c: Context\n    ) {\n        val theme = c.theme\n\n        description.isEnabled = false\n        enableScroll()\n        isHighlightPerTapEnabled = true\n        setNoDataTextColor(theme.getColor(R.attr.colorControlNormal))\n        legend.isEnabled = false\n\n        axisLeft.isEnabled = false\n        axisRight.isEnabled = false\n        xAxis.apply {\n            position = XAxis.XAxisPosition.BOTTOM\n            textColor = theme.getColor(R.attr.colorOnSurface)\n            setDrawAxisLine(false)\n            setDrawGridLines(false)\n            granularity = 1f\n        }\n\n        setPinchZoom(false)\n        isDoubleTapToZoomEnabled = false\n        setDrawGridBackground(false)\n    }\n\n    fun BarDataSet.applyStyle(c: Context, colorArray: List<Int>) {\n        valueFormatter = NonZeroLargeValueFormatter()\n        isHighlightEnabled = false\n        applyBaseStyle(c, colorArray)\n    }\n\n    fun BarData.applyStyle(c: Context) {\n        barWidth = 0.9f\n        applyBaseStyle(c)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/chart/BaseChartStyle.kt",
    "content": "package io.github.drumber.kitsune.ui.component.chart\n\nimport android.content.Context\nimport androidx.annotation.ArrayRes\nimport com.github.mikephil.charting.data.BaseDataSet\nimport com.github.mikephil.charting.data.ChartData\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.util.extensions.getColor\n\nabstract class BaseChartStyle {\n\n    protected fun BaseDataSet<*>.applyBaseStyle(\n        c: Context,\n        colorArray: List<Int> = getColorArray(c, R.array.stats_chart_colors)\n    ) {\n        setDrawIcons(false)\n        colors = colorArray\n    }\n\n    fun ChartData<*>.applyBaseStyle(c: Context) {\n        setValueTextSize(11f)\n        setValueTextColor(c.theme.getColor(R.attr.colorOnSurface))\n    }\n\n    fun getColorArray(c: Context, @ArrayRes colorArray: Int): List<Int> {\n        val colors = c.resources.obtainTypedArray(colorArray)\n        val list = mutableListOf<Int>()\n        for (i in 0 until colors.length()) {\n            list += colors.getColor(i, 0)\n        }\n        colors.recycle()\n        return list\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/chart/CustomPercentFormatter.kt",
    "content": "package io.github.drumber.kitsune.ui.component.chart\n\nimport com.github.mikephil.charting.data.PieEntry\nimport com.github.mikephil.charting.formatter.ValueFormatter\nimport java.text.DecimalFormat\n\nclass CustomPercentFormatter : ValueFormatter() {\n\n    private val format = DecimalFormat(\"##%\").apply {\n        multiplier = 1\n    }\n\n    override fun getFormattedValue(value: Float): String {\n        return format.format(value)\n    }\n\n    override fun getPieLabel(value: Float, pieEntry: PieEntry?): String {\n        return getFormattedValue(value)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/chart/NonZeroLargeValueFormatter.kt",
    "content": "package io.github.drumber.kitsune.ui.component.chart\n\nimport com.github.mikephil.charting.formatter.LargeValueFormatter\n\nclass NonZeroLargeValueFormatter : LargeValueFormatter() {\n\n    override fun getFormattedValue(value: Float): String {\n        return if (value > 0f) {\n            super.getFormattedValue(value)\n        } else {\n            \"\"\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/chart/PieChartStyle.kt",
    "content": "package io.github.drumber.kitsune.ui.component.chart\n\nimport android.content.Context\nimport androidx.annotation.StringRes\nimport com.github.mikephil.charting.animation.Easing\nimport com.github.mikephil.charting.charts.PieChart\nimport com.github.mikephil.charting.components.Legend\nimport com.github.mikephil.charting.data.PieData\nimport com.github.mikephil.charting.data.PieDataSet\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.util.extensions.getColor\n\nobject PieChartStyle : BaseChartStyle() {\n\n    const val STATS_MAX_ELEMENTS = 7\n    const val ANIMATION_DURATION = 1000\n\n    fun PieChart.applyStyle(\n        c: Context,\n        @StringRes centerTextResId: Int? = null,\n        showLegend: Boolean = false\n    ) {\n        val theme = c.theme\n\n        setUsePercentValues(false)\n        description.isEnabled = false\n        setExtraOffsets(5f, 5f, 5f, 5f)\n\n        enableScroll()\n        isRotationEnabled = false\n\n        isDrawHoleEnabled = true\n        setHoleColor(theme.getColor(android.R.color.transparent))\n        holeRadius = 55f\n        setTransparentCircleAlpha(50)\n\n        setCenterTextColor(theme.getColor(R.attr.colorOnSurface))\n        if (centerTextResId != null) {\n            centerText = c.getString(centerTextResId)\n        }\n\n        isHighlightPerTapEnabled = true\n\n        setNoDataTextColor(theme.getColor(R.attr.colorControlNormal))\n        setEntryLabelColor(theme.getColor(R.attr.colorOnSurface))\n\n        animateY(ANIMATION_DURATION, Easing.EaseInOutQuad)\n\n        legend.apply {\n            form = Legend.LegendForm.CIRCLE\n            verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM\n            horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT\n            orientation = Legend.LegendOrientation.VERTICAL\n            setDrawInside(false)\n            textColor = theme.getColor(R.attr.colorOnSurface)\n            yEntrySpace = 2f\n            yOffset = 0f\n            xOffset = 0f\n            isEnabled = showLegend\n        }\n    }\n\n    fun PieDataSet.applyStyle(c: Context) {\n        applyBaseStyle(c)\n        sliceSpace = 0f\n        selectionShift = 5f\n    }\n\n    fun PieData.applyStyle(c: Context) {\n        applyBaseStyle(c)\n        setValueFormatter(CustomPercentFormatter())\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/component/chart/StepAxisValueFormatter.kt",
    "content": "package io.github.drumber.kitsune.ui.component.chart\n\nimport com.github.mikephil.charting.components.AxisBase\nimport com.github.mikephil.charting.formatter.ValueFormatter\nimport java.text.DecimalFormat\n\nclass StepAxisValueFormatter(\n    private val startValue: Float,\n    private val stepSize: Float\n) : ValueFormatter() {\n\n    override fun getAxisLabel(value: Float, axis: AxisBase?): String {\n        return DecimalFormat(\"#.##\").format(startValue + stepSize * value)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/DetailsFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.details\n\nimport android.content.Intent\nimport android.graphics.Color\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport android.os.Bundle\nimport android.text.SpannableString\nimport android.text.style.ForegroundColorSpan\nimport android.text.style.UnderlineSpan\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.core.os.bundleOf\nimport androidx.core.view.children\nimport androidx.core.view.doOnPreDraw\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.setFragmentResultListener\nimport androidx.lifecycle.lifecycleScope\nimport androidx.navigation.fragment.FragmentNavigatorExtras\nimport androidx.navigation.fragment.findNavController\nimport androidx.navigation.fragment.navArgs\nimport androidx.vectordrawable.graphics.drawable.Animatable2Compat\nimport androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.load.resource.bitmap.RoundedCorners\nimport com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions\nimport com.github.mikephil.charting.data.BarData\nimport com.github.mikephil.charting.data.BarDataSet\nimport com.github.mikephil.charting.data.BarEntry\nimport com.google.android.material.chip.Chip\nimport com.google.android.material.elevation.SurfaceColors\nimport com.google.android.material.navigation.NavigationBarView\nimport com.google.android.material.snackbar.Snackbar\nimport com.google.android.material.transition.MaterialContainerTransform\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.addTransform\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.constants.SortFilter\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.en\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.common.withoutCommonTitles\nimport io.github.drumber.kitsune.data.presentation.dto.toMedia\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaDto\nimport io.github.drumber.kitsune.data.presentation.getStringRes\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.library.getStringResId\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.presentation.model.media.MediaSelector\nimport io.github.drumber.kitsune.data.presentation.model.media.category.Category\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\nimport io.github.drumber.kitsune.databinding.FragmentDetailsBinding\nimport io.github.drumber.kitsune.databinding.ItemDetailsInfoRowBinding\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.adapter.MediaRelationshipRecyclerViewAdapter\nimport io.github.drumber.kitsune.ui.adapter.StreamingLinkAdapter\nimport io.github.drumber.kitsune.ui.authentication.AuthenticationActivity\nimport io.github.drumber.kitsune.ui.base.BaseFragment\nimport io.github.drumber.kitsune.ui.component.chart.BarChartStyle\nimport io.github.drumber.kitsune.ui.component.chart.BarChartStyle.applyStyle\nimport io.github.drumber.kitsune.ui.component.chart.StepAxisValueFormatter\nimport io.github.drumber.kitsune.ui.details.LibraryChangeResult.AddNewLibraryEntryFailed\nimport io.github.drumber.kitsune.ui.details.LibraryChangeResult.DeleteLibraryEntryFailed\nimport io.github.drumber.kitsune.ui.details.LibraryChangeResult.LibraryUpdateResult\nimport io.github.drumber.kitsune.util.DataUtil.mapLanguageCodesToDisplayName\nimport io.github.drumber.kitsune.util.extensions.getColor\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.extensions.openPhotoViewActivity\nimport io.github.drumber.kitsune.util.extensions.showSomethingWrongToast\nimport io.github.drumber.kitsune.util.extensions.startUrlShareIntent\nimport io.github.drumber.kitsune.util.extensions.toPx\nimport io.github.drumber.kitsune.util.logW\nimport io.github.drumber.kitsune.util.rating.RatingFrequenciesUtil.calculateAverageRating\nimport io.github.drumber.kitsune.util.rating.RatingFrequenciesUtil.transformToRatingSystem\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.convertFrom\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.stepSize\nimport io.github.drumber.kitsune.util.ui.initMarginWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.showSnackbar\nimport io.github.drumber.kitsune.util.ui.showSnackbarOnFailure\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\nimport java.text.NumberFormat\nimport java.util.concurrent.CopyOnWriteArrayList\nimport kotlin.math.roundToInt\n\nclass DetailsFragment : BaseFragment(R.layout.fragment_details, true),\n    NavigationBarView.OnItemReselectedListener {\n\n    private val args: DetailsFragmentArgs by navArgs()\n\n    private var _binding: FragmentDetailsBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: DetailsViewModel by viewModel()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val transition = MaterialContainerTransform().apply {\n            drawingViewId = R.id.nav_host_fragment\n            duration = resources.getInteger(R.integer.material_motion_duration_short_2).toLong()\n            scrimColor = Color.TRANSPARENT\n            setAllContainerColors(SurfaceColors.SURFACE_0.getColor(requireContext()))\n        }\n        sharedElementEnterTransition = transition\n        sharedElementReturnTransition = transition\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentDetailsBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        postponeEnterTransition()\n        view.doOnPreDraw { startPostponedEnterTransition() }\n\n        if (args.media != null) {\n            viewModel.initMediaModel(args.media!!.toMedia())\n        } else if (!args.type.isNullOrBlank() && !args.slug.isNullOrBlank()) {\n            val isAnime = when (args.type!!.lowercase()) {\n                \"anime\" -> true\n                \"manga\" -> false\n                else -> null\n            }\n\n            if (isAnime == null) {\n                logW(\"Unknown media type '${args.type}'.\")\n                showSomethingWrongToast()\n                goBack()\n            } else {\n                viewModel.initFromDeepLink(isAnime, args.slug!!)\n            }\n        } else {\n            logW(\"DetailsFragment opened without media bundle or invalid deeplink parameters.\")\n            showSomethingWrongToast()\n            goBack()\n        }\n\n        initAppBar()\n\n        viewModel.mediaModel.observe(viewLifecycleOwner) { model ->\n            binding.data = model\n            updateTitlesInDetailsTable(model.titles)\n            showCategoryChips(model)\n            showFranchise(model)\n            showStreamingLinks(model)\n            showRatingChart(model)\n\n            val glide = Glide.with(this)\n\n            glide.load(model.coverImageUrl)\n                .transition(DrawableTransitionOptions.withCrossFade())\n                .placeholder(R.drawable.cover_placeholder)\n                .into(binding.ivCover)\n\n            glide.load(model.posterImageUrl)\n                .addTransform(RoundedCorners(8.toPx()))\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivThumbnail)\n\n        }\n\n        binding.ivThumbnail.setOnClickListener {\n            viewModel.mediaModel.value?.let { media ->\n                val title = media.title\n                media.posterImage?.originalOrDown()?.let { imageUrl ->\n                    openPhotoViewActivity(\n                        imageUrl,\n                        title,\n                        media.posterImageUrl,\n                        binding.ivThumbnail\n                    )\n                }\n            }\n        }\n\n        binding.ivCover.setOnClickListener {\n            viewModel.mediaModel.value?.let { media ->\n                val title = media.title\n                media.coverImage?.originalOrDown()?.let { imageUrl ->\n                    openPhotoViewActivity(imageUrl, title, media.coverImageUrl, binding.ivCover)\n                }\n            }\n        }\n\n        viewModel.libraryEntryWrapper.observe(viewLifecycleOwner) { libraryEntryWithModification ->\n            val isManga = libraryEntryWithModification?.libraryEntry?.media is Manga\n                    || viewModel.mediaModel.value is Manga\n            if (libraryEntryWithModification != null) {\n                libraryEntryWithModification.status?.let { status ->\n                    binding.btnManageLibrary.setText(status.getStringResId(!isManga))\n                } ?: binding.btnManageLibrary.setText(R.string.library_action_add)\n                binding.libraryEntry = libraryEntryWithModification\n            } else {\n                // reset to defaults\n                binding.btnManageLibrary.setText(R.string.library_action_add)\n                binding.libraryEntry = null\n            }\n        }\n\n        viewModel.favorite.observe(viewLifecycleOwner) { favorite ->\n            val isFavorite = favorite != null\n            updateFavoriteIcon(isFavorite)\n        }\n\n        viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->\n            binding.progressIndicator.isVisible = isLoading\n        }\n\n        binding.apply {\n            content.initPaddingWindowInsetsListener(left = true, right = true, bottom = true)\n            btnManageLibrary.setOnClickListener { showManageLibraryBottomSheet() }\n            btnMediaUnits.setOnClickListener {\n                val media = viewModel.mediaModel.value ?: return@setOnClickListener\n                val action = DetailsFragmentDirections.actionDetailsFragmentToEpisodesFragment(\n                    media.toMediaDto()\n                )\n                findNavController().navigate(action)\n            }\n            btnCharacters.setOnClickListener {\n                val media = viewModel.mediaModel.value ?: return@setOnClickListener\n                val action = DetailsFragmentDirections.actionDetailsFragmentToCharactersFragment(\n                    media.id,\n                    media is Anime\n                )\n                findNavController().navigate(action)\n            }\n\n            btnEditLibraryEntry.setOnClickListener { showEditLibraryEntryFragment() }\n\n            btnRatingTypeMenu.setOnClickListener { v ->\n                showRatingTypeMenu(v)\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.libraryChangeResultFlow.collectLatest {\n                when (it) {\n                    is LibraryUpdateResult -> it.result.showSnackbarOnFailure(binding.btnManageLibrary)\n                    is AddNewLibraryEntryFailed -> showSnackbar(\n                        binding.btnManageLibrary,\n                        R.string.error_library_add_failed\n                    )\n\n                    is DeleteLibraryEntryFailed -> showSnackbar(\n                        binding.btnManageLibrary,\n                        R.string.error_library_delete_failed\n                    )\n                }\n            }\n        }\n\n        setFragmentResultListener(ManageLibraryBottomSheet.STATUS_REQUEST_KEY) { _, bundle ->\n            val libraryEntryStatus =\n                bundle.get(ManageLibraryBottomSheet.BUNDLE_STATUS) as? LibraryStatus\n            libraryEntryStatus?.let { viewModel.updateLibraryEntryStatus(it) }\n        }\n\n        setFragmentResultListener(ManageLibraryBottomSheet.REMOVE_REQUEST_KEY) { _, bundle ->\n            val shouldRemove = !bundle.getBoolean(ManageLibraryBottomSheet.BUNDLE_EXISTS_IN_LIBRARY)\n            if (shouldRemove) {\n                viewModel.removeLibraryEntry()\n            }\n        }\n    }\n\n    private fun initAppBar() {\n        binding.apply {\n            toolbar.setNavigationOnClickListener { goBack() }\n            toolbar.setOnMenuItemClickListener { item ->\n                when (item.itemId) {\n                    R.id.menu_share_media -> {\n                        val url = viewModel.mediaModel.value?.let {\n                            val prefix =\n                                if (it is Anime) Kitsu.ANIME_URL_PREFIX else Kitsu.MANGA_URL_PREFIX\n                            prefix + it.slug\n                        }\n                        if (url != null) {\n                            startUrlShareIntent(url)\n                        } else {\n                            showSomethingWrongToast()\n                        }\n                    }\n\n                    R.id.menu_favorite -> {\n                        if (viewModel.isLoggedIn()) {\n                            // update icon immediately before waiting for response\n                            val willBeFavorite = viewModel.favorite.value == null\n                            updateFavoriteIcon(willBeFavorite, true)\n                            // send update to server\n                            viewModel.toggleFavorite()\n                        } else {\n                            showLogInSnackbar()\n                        }\n                    }\n\n                    R.id.menu_open_external -> {\n                        viewModel.loadMappingsIfNotAlreadyLoaded()\n                        val mappingsBottomSheet = MediaMappingsBottomSheet()\n                        mappingsBottomSheet.show(childFragmentManager, MediaMappingsBottomSheet.TAG)\n                    }\n                }\n                true\n            }\n\n            collapsingToolbar.initWindowInsetsListener(consume = false)\n            toolbar.initWindowInsetsListener(consume = false)\n        }\n    }\n\n    private fun updateFavoriteIcon(isFavorite: Boolean, isUserAction: Boolean = false) {\n        val menuItem = binding.toolbar.menu.findItem(R.id.menu_favorite)\n\n        if (isFavorite && isUserAction) {\n            AnimatedVectorDrawableCompat.create(\n                requireContext(),\n                R.drawable.animated_favorite\n            )?.apply {\n                menuItem.icon = this\n                registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {\n                    override fun onAnimationEnd(drawable: Drawable?) {\n                        menuItem.setIcon(R.drawable.ic_favorite_24)\n                    }\n                })\n                start()\n            }\n        } else if (menuItem.icon !is AnimatedVectorDrawableCompat || !isFavorite) {\n            menuItem.setIcon(\n                if (isFavorite) R.drawable.ic_favorite_24\n                else R.drawable.ic_favorite_border_24\n            )\n        }\n\n        menuItem.setTitle(\n            if (isFavorite)\n                R.string.action_remove_from_favorites\n            else\n                R.string.action_add_to_favorites\n        )\n    }\n\n    private fun updateTitlesInDetailsTable(titles: Titles?) {\n        val identifierTag = \"dynamic_title\"\n        val tableLayout = binding.sectionDetailsInfo.tableLayout\n        // remove any previous added titles\n        tableLayout.apply {\n            children.filter { it.tag == identifierTag }.toList().forEach { removeView(it) }\n        }\n\n        // map language codes and sort them\n        val sortedTitles = titles?.withoutCommonTitles()\n            ?.filterValues { !it.isNullOrBlank() }\n            ?.filterNot { it.key == \"en_us\" && it.value == titles.en }\n            ?.mapLanguageCodesToDisplayName()\n            ?.toList()\n            ?.sortedByDescending { it.first }\n\n        if (sortedTitles.isNullOrEmpty()) return\n\n        val maxShownTitles = 3\n        val shouldLimitShownTitles = sortedTitles.size > maxShownTitles &&\n                !viewModel.areAllTileLanguagesShown\n        val rowIndex = tableLayout.indexOfChild(binding.sectionDetailsInfo.synonymsRowLayout.root)\n            .coerceAtLeast(0)\n        // add a row for each title\n        sortedTitles\n            .takeLast(if (shouldLimitShownTitles) maxShownTitles else Int.MAX_VALUE)\n            .forEach { (language, title) ->\n                val rowBinding = ItemDetailsInfoRowBinding.inflate(layoutInflater)\n                rowBinding.title = language\n                rowBinding.value = title\n                rowBinding.root.tag = identifierTag\n                tableLayout.addView(rowBinding.root, rowIndex)\n            }\n\n        // add 'show more' text to table\n        if (sortedTitles.size > maxShownTitles) {\n            val showMoreRow = createShowMoreTitlesRow()\n            showMoreRow.tag = identifierTag\n            val viewIndex =\n                tableLayout.indexOfChild(binding.sectionDetailsInfo.synonymsRowLayout.root)\n                    .coerceAtLeast(0)\n            tableLayout.addView(showMoreRow, viewIndex)\n        }\n    }\n\n    private fun createShowMoreTitlesRow(): View {\n        val rowBinding = ItemDetailsInfoRowBinding.inflate(layoutInflater)\n        val text = getString(\n            if (viewModel.areAllTileLanguagesShown)\n                R.string.action_show_less\n            else\n                R.string.action_show_more\n        )\n        rowBinding.title = SpannableString(text).apply {\n            setSpan(\n                ForegroundColorSpan(requireActivity().theme.getColor(R.attr.colorPrimary)),\n                0,\n                text.length,\n                SpannableString.SPAN_INCLUSIVE_EXCLUSIVE\n            )\n            setSpan(UnderlineSpan(), 0, text.length, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)\n        }\n        rowBinding.tvTitle.setOnClickListener {\n            viewModel.areAllTileLanguagesShown = !viewModel.areAllTileLanguagesShown\n            updateTitlesInDetailsTable(viewModel.mediaModel.value?.titles)\n        }\n        return rowBinding.root\n    }\n\n    private fun showCategoryChips(media: Media) {\n        if (!media.categories.isNullOrEmpty()) {\n            binding.chipGroupCategories.removeAllViews()\n\n            media.categories.orEmpty()\n                .sortedBy { it.title }\n                .forEach { category ->\n                    val chip = Chip(requireContext())\n                    chip.text = category.title\n                    chip.setOnClickListener {\n                        onCategoryChipClicked(category, media)\n                    }\n                    binding.chipGroupCategories.addView(chip)\n                }\n        }\n    }\n\n    private fun onCategoryChipClicked(category: Category, media: Media) {\n        val categorySlug = category.slug ?: return\n        val title = category.title ?: getString(R.string.no_information)\n\n        val mediaSelector = MediaSelector(\n            if (media is Anime) MediaType.Anime else MediaType.Manga,\n            Filter()\n                .filter(\"categories\", categorySlug)\n                .sort(SortFilter.POPULARITY_DESC.queryParam)\n                .options\n        )\n\n        val action =\n            DetailsFragmentDirections.actionDetailsFragmentToMediaListFragment(mediaSelector, title)\n        findNavController().navigate(action)\n    }\n\n    private fun showFranchise(media: Media) {\n        val data = media.mediaRelationships?.sortedBy { it.role?.ordinal } ?: emptyList()\n\n        if (binding.rvFranchise.adapter !is MediaRelationshipRecyclerViewAdapter) {\n            val glide = Glide.with(this)\n            val adapter = MediaRelationshipRecyclerViewAdapter(\n                CopyOnWriteArrayList(data),\n                glide\n            ) { view, clickedMedia ->\n                clickedMedia.media?.let { onFranchiseItemClicked(view, it) }\n            }\n            binding.rvFranchise.adapter = adapter\n        } else {\n            val adapter = binding.rvFranchise.adapter as MediaRelationshipRecyclerViewAdapter\n            adapter.dataSet.addAll(0, data)\n            adapter.notifyDataSetChanged()\n        }\n    }\n\n    private fun onFranchiseItemClicked(view: View, media: Media) {\n        val action = DetailsFragmentDirections.actionDetailsFragmentSelf(media.toMediaDto())\n        val detailsTransitionName = getString(R.string.details_poster_transition_name)\n        val extras = FragmentNavigatorExtras(view to detailsTransitionName)\n        findNavController().navigateSafe(R.id.details_fragment, action, extras)\n    }\n\n    private fun showStreamingLinks(media: Media) {\n        val data = (media as? Anime)?.streamingLinks ?: emptyList()\n\n        if (binding.rvStreamer.adapter !is StreamingLinkAdapter) {\n            val glide = Glide.with(this)\n            val adapter =\n                StreamingLinkAdapter(CopyOnWriteArrayList(data), glide) { _, streamingLink ->\n                    streamingLink.url?.let { url ->\n                        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))\n                        startActivity(intent)\n                    }\n                }\n            binding.rvStreamer.adapter = adapter\n        } else {\n            val adapter = binding.rvStreamer.adapter as StreamingLinkAdapter\n            adapter.dataSet.addAll(0, data)\n            adapter.notifyDataSetChanged()\n        }\n    }\n\n    private fun showRatingChart(media: Media) {\n        val ratings = media.ratingFrequencies ?: return\n        val ratingSystem = KitsunePref.ratingChartRatingSystem\n\n        val ratingList = ratings.transformToRatingSystem(ratingSystem)\n\n        val chartEntries = ratingList.mapIndexed { index, value ->\n            BarEntry(index.toFloat(), value.toFloat())\n        }\n\n        val dataSet = BarDataSet(chartEntries, \"Ratings\")\n        val chartColorArray = BarChartStyle\n            .getColorArray(requireContext(), R.array.ratings_chart_colors)\n            .let { colorArray ->\n                val colorStep = (colorArray.size.toFloat() / ratingList.size).roundToInt()\n                colorArray.filterIndexed { index, _ ->\n                    index % colorStep == 0\n                }\n            }\n        dataSet.applyStyle(requireContext(), chartColorArray)\n\n        val barData = BarData(dataSet)\n        barData.applyStyle(requireContext())\n\n        binding.chartRatings.apply {\n            data = barData\n            applyStyle(requireContext())\n            setFitBars(true)\n            xAxis.valueFormatter = StepAxisValueFormatter(\n                ratingSystem.convertFrom(2),\n                ratingSystem.stepSize()\n            )\n            xAxis.labelCount = ratingList.size\n            invalidate()\n        }\n\n        val avgRating = ratings.calculateAverageRating(ratingSystem)\n        val numberFormatter = NumberFormat.getNumberInstance()\n        numberFormatter.minimumFractionDigits = 1\n        numberFormatter.maximumFractionDigits = 2\n        binding.tvCalculatedAverageRating.text = numberFormatter.format(avgRating)\n        binding.tvCalculatedAverageRatingMax.text = \"/ \" + numberFormatter.format(ratingSystem.convertFrom(20))\n    }\n\n    private fun showRatingTypeMenu(anchorView: View) {\n        val popup = PopupMenu(requireContext(), anchorView)\n        val menu = popup.menu\n        val selectedRatingSystem = KitsunePref.ratingChartRatingSystem\n\n        LocalRatingSystemPreference.entries.forEach {\n            val menuItem = menu.add(1, it.ordinal, it.ordinal, it.getStringRes())\n            menuItem.isChecked = selectedRatingSystem == it\n            menuItem.setOnMenuItemClickListener { _ ->\n                KitsunePref.ratingChartRatingSystem = it\n                viewModel.mediaModel.value?.let { mediaAdapter -> showRatingChart(mediaAdapter) }\n                true\n            }\n        }\n\n        menu.setGroupCheckable(1, true, true)\n        popup.show()\n    }\n\n    private fun showManageLibraryBottomSheet() {\n        if (viewModel.isLoggedIn()) {\n            viewModel.mediaModel.value?.let { mediaAdapter ->\n                val sheetManageLibrary = ManageLibraryBottomSheet()\n                val bundle = bundleOf(\n                    ManageLibraryBottomSheet.BUNDLE_TITLE to mediaAdapter.title,\n                    ManageLibraryBottomSheet.BUNDLE_IS_ANIME to (mediaAdapter is Anime),\n                    ManageLibraryBottomSheet.BUNDLE_EXISTS_IN_LIBRARY to (viewModel.libraryEntryWrapper.value != null)\n                )\n                sheetManageLibrary.arguments = bundle\n                sheetManageLibrary.show(parentFragmentManager, ManageLibraryBottomSheet.TAG)\n            }\n        } else {\n            showLogInSnackbar()\n        }\n    }\n\n    private fun showEditLibraryEntryFragment() {\n        if (!viewModel.isLoggedIn()) return\n        val entryId = viewModel.libraryEntryWrapper.value?.libraryEntry?.id ?: return\n        val action =\n            DetailsFragmentDirections.actionDetailsFragmentToLibraryEditEntryFragment(entryId)\n        findNavController().navigateSafe(R.id.details_fragment, action)\n    }\n\n    private fun showLogInSnackbar() {\n        Snackbar.make(\n            binding.btnManageLibrary,\n            R.string.info_log_in_required,\n            Snackbar.LENGTH_LONG\n        ).apply {\n            view.initMarginWindowInsetsListener(left = true, right = true, bottom = true)\n            setAction(R.string.action_log_in) {\n                val intent = Intent(requireActivity(), AuthenticationActivity::class.java)\n                startActivity(intent)\n            }\n        }.show()\n    }\n\n    private fun goBack() {\n        findNavController().navigateUp()\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        if (binding.nsvContent.canScrollVertically(-1)) {\n            binding.nsvContent.smoothScrollTo(0, 0)\n            binding.appBarLayout.setExpanded(true)\n        } else {\n            goBack()\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/DetailsViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.details\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MediatorLiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.data.common.exception.ResourceUpdateFailed\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.mapping.Mapping\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.presentation.model.user.Favorite\nimport io.github.drumber.kitsune.data.repository.AnimeRepository\nimport io.github.drumber.kitsune.data.repository.FavoriteRepository\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.data.repository.MangaRepository\nimport io.github.drumber.kitsune.data.repository.MappingRepository\nimport io.github.drumber.kitsune.domain.auth.IsUserLoggedInUseCase\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryUseCase\nimport io.github.drumber.kitsune.domain.user.GetLocalUserIdUseCase\nimport io.github.drumber.kitsune.ui.details.LibraryChangeResult.AddNewLibraryEntryFailed\nimport io.github.drumber.kitsune.ui.details.LibraryChangeResult.DeleteLibraryEntryFailed\nimport io.github.drumber.kitsune.ui.details.LibraryChangeResult.LibraryUpdateResult\nimport io.github.drumber.kitsune.util.logD\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.logW\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nclass DetailsViewModel(\n    private val getLocalUserId: GetLocalUserIdUseCase,\n    private val isUserLoggedIn: IsUserLoggedInUseCase,\n    private val updateLibraryEntry: UpdateLibraryEntryUseCase,\n    private val favoriteRepository: FavoriteRepository,\n    private val libraryRepository: LibraryRepository,\n    private val animeRepository: AnimeRepository,\n    private val mangaRepository: MangaRepository,\n    private val mappingRepository: MappingRepository\n) : ViewModel() {\n\n    fun isLoggedIn() = isUserLoggedIn()\n\n    private val _mediaModel = MutableLiveData<Media>()\n    val mediaModel: LiveData<Media>\n        get() = _mediaModel\n\n    /** Combines local cached and fetched library entry. */\n    private val _libraryEntryWithModification = MediatorLiveData<LibraryEntryWithModification?>()\n    val libraryEntryWrapper: LiveData<LibraryEntryWithModification?>\n        get() = _libraryEntryWithModification\n\n    private val _favorite = MutableLiveData<Favorite?>()\n    val favorite: LiveData<Favorite?>\n        get() = _favorite\n\n    private val _mappingsSate = MutableStateFlow<MediaMappingsSate>(MediaMappingsSate.Initial)\n    val mappingsSate\n        get() = _mappingsSate.asStateFlow()\n\n    private val _isLoading = MutableLiveData(false)\n    val isLoading: LiveData<Boolean>\n        get() = _isLoading\n\n    private val acceptInternalAction: (InternalAction) -> Unit\n\n    val libraryChangeResultFlow: Flow<LibraryChangeResult>\n\n    var areAllTileLanguagesShown = false\n\n    init {\n        val internalActionFlow = MutableSharedFlow<InternalAction>()\n        libraryChangeResultFlow = internalActionFlow.mapNotNull { action ->\n            when (action) {\n                is InternalAction.LibraryUpdateResult -> LibraryUpdateResult(action.result)\n                is InternalAction.AddNewLibraryEntryFailed -> AddNewLibraryEntryFailed\n                is InternalAction.DeleteLibraryEntryFailed -> DeleteLibraryEntryFailed\n            }\n        }\n\n        acceptInternalAction = { action ->\n            viewModelScope.launch { internalActionFlow.emit(action) }\n        }\n    }\n\n    fun initFromDeepLink(isAnime: Boolean, slug: String) {\n        val filter = Filter()\n            .filter(\"slug\", slug)\n            .fields(\"media\", \"id\")\n\n        viewModelScope.launch(Dispatchers.IO) {\n            val media = try {\n                if (isAnime) {\n                    animeRepository.getAllAnime(filter)\n                } else {\n                    mangaRepository.getAllManga(filter)\n                }\n            } catch (e: Exception) {\n                logE(\"Failed to load media from deep link.\", e)\n                null\n            }\n\n            if (media.isNullOrEmpty()) {\n                logW(\"No media for slug '$slug' found.\")\n                return@launch\n            }\n\n            withContext(Dispatchers.Main) {\n                initMediaModel(media.first())\n            }\n        }\n    }\n\n    fun initMediaModel(media: Media) {\n        if (_mediaModel.value == null) {\n            _mediaModel.value = media\n            _isLoading.value = true\n            viewModelScope.launch(Dispatchers.IO) {\n                libraryRepository.getLibraryEntryFromMedia(media.id)?.media?.let {\n                    // display media model from local library entry if available\n                    _mediaModel.postValue(it)\n                }\n                awaitAll(\n                    async { loadFullMedia(media) },\n                    async { loadLibraryEntry(media) },\n                    async { loadFavorite(media) }\n                )\n                _isLoading.postValue(false)\n            }\n        }\n    }\n\n    private suspend fun loadFullMedia(media: Media) {\n        val id = media.id\n        val filter = Filter()\n            .fields(\"categories\", \"slug\", \"title\")\n\n        val commonIncludes = arrayOf(\n            \"categories\",\n            \"mediaRelationships\",\n            \"mediaRelationships.destination\"\n        )\n\n        try {\n            val fullMedia = if (media is Anime) {\n                filter.include(\n                    *commonIncludes,\n                    \"animeProductions.producer\",\n                    \"streamingLinks\",\n                    \"streamingLinks.streamer\"\n                )\n                animeRepository.getAnime(id, filter)\n            } else {\n                filter.include(*commonIncludes)\n                mangaRepository.getManga(id, filter)\n            } ?: throw NoDataException(\"Received data is null.\")\n\n            _mediaModel.postValue(fullMedia)\n        } catch (e: Exception) {\n            logE(\"Failed to load full media model.\", e)\n        }\n    }\n\n    private suspend fun loadLibraryEntry(media: Media) {\n        val userId = getLocalUserId() ?: return\n\n        // add local database as library entry source\n        viewModelScope.launch(Dispatchers.Main) {\n            _libraryEntryWithModification.addSource(libraryRepository.getLibraryEntryWithModificationFromMediaAsLiveData(media.id)) {\n                _libraryEntryWithModification.value = it\n            }\n        }\n        val filter = Filter()\n            .filter(\"user_id\", userId)\n            .fields(\"libraryEntries\", \"status\", \"progress\", \"ratingTwenty\")\n            .pageLimit(1)\n\n        if (media is Anime) {\n            filter.filter(\"anime_id\", media.id)\n        } else {\n            filter.filter(\"manga_id\", media.id)\n        }\n\n        try {\n            // fetch library entry from the server\n            val libraryEntries = libraryRepository.fetchAllLibraryEntries(filter)\n            if (!libraryEntries.isNullOrEmpty()) {\n                // post fetched library entry that is possibly more up-to-date than the local cached one\n                _libraryEntryWithModification.postValue(\n                    LibraryEntryWithModification(libraryEntries.first(), null)\n                )\n            } else if (libraryEntryWrapper.value != null) {\n                // library entry is not available on the server but it is in the local cache, was it deleted?\n                // -> local database cache is out of sync, remove entry from database\n                libraryEntryWrapper.value?.let { wrapper ->\n                    logD(\n                        \"There is no library entry on the server, but it exists in the local cache. \" +\n                                \"Removed it from local database...\"\n                    )\n                    withContext(Dispatchers.IO) {\n                        _libraryEntryWithModification.postValue(null)\n                        libraryRepository.mayRemoveLibraryEntryLocally(wrapper.libraryEntry.id)\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            logE(\"Failed to load library entry.\", e)\n        }\n    }\n\n    private suspend fun loadFavorite(media: Media) {\n        val userId = getLocalUserId() ?: return\n\n        val filter = Filter()\n            .filter(\"user_id\", userId)\n            .filter(\"item_id\", media.id)\n            .filter(\"item_type\", if (media is Anime) \"Anime\" else \"Manga\")\n\n        try {\n            val favorites = favoriteRepository.getAllFavorites(filter)\n            _favorite.postValue(favorites?.firstOrNull())\n        } catch (e: Exception) {\n            logE(\"Failed to load favorites.\", e)\n        }\n    }\n\n    fun loadMappingsIfNotAlreadyLoaded() {\n        if (_mappingsSate.value != MediaMappingsSate.Initial) return\n        val mediaModel = mediaModel.value ?: return\n\n        viewModelScope.launch(Dispatchers.IO) {\n            _mappingsSate.value = MediaMappingsSate.Loading\n\n            val mappingsState = try {\n                val mappings = if (mediaModel is Anime) {\n                    mappingRepository.getAnimeMappings(mediaModel.id)\n                } else {\n                    mappingRepository.getMangaMappings(mediaModel.id)\n                } ?: emptyList()\n\n                val mappingsWithKitsu = mappings + Mapping(\n                    id = \"\",\n                    externalSite = \"kitsu/\" + if (mediaModel is Anime) \"anime\" else \"manga\",\n                    externalId = mediaModel.slug ?: mediaModel.id\n                )\n\n                MediaMappingsSate.Success(mappingsWithKitsu)\n            } catch (e: Exception) {\n                logE(\"Failed to load mappings.\", e)\n                MediaMappingsSate.Error(e.message ?: \"Failed to load mappings.\")\n            }\n\n            _mappingsSate.value = mappingsState\n        }\n    }\n\n    fun updateLibraryEntryStatus(status: LibraryStatus) {\n        val userId = getLocalUserId() ?: return\n        val mediaModel = mediaModel.value ?: return\n        val existingLibraryEntryId = libraryEntryWrapper.value?.libraryEntry?.id\n\n        viewModelScope.launch(Dispatchers.IO) {\n            if (existingLibraryEntryId.isNullOrBlank()) { // post new library entry\n                try {\n                    val newLibraryEntry = libraryRepository.addNewLibraryEntry(\n                        userId,\n                        mediaModel,\n                        status\n                    ) ?: throw NoDataException(\"Failed to post new library entry.\")\n                    _libraryEntryWithModification.postValue(\n                        LibraryEntryWithModification(newLibraryEntry, null)\n                    )\n                } catch (e: Exception) {\n                    logE(\"Failed to add new library entry.\", e)\n                    acceptInternalAction(InternalAction.AddNewLibraryEntryFailed)\n                }\n            } else { // update existing library entry\n                val modification = LibraryEntryModification\n                    .withIdAndNulls(existingLibraryEntryId)\n                    .copy(status = status)\n                val result = updateLibraryEntry(modification)\n                acceptInternalAction(InternalAction.LibraryUpdateResult(result))\n            }\n        }\n    }\n\n    fun removeLibraryEntry() {\n        val libraryEntryId = libraryEntryWrapper.value?.libraryEntry?.id ?: return\n        viewModelScope.launch(Dispatchers.IO) {\n            val isDeleted = try {\n                libraryRepository.removeLibraryEntry(libraryEntryId)\n                true\n            } catch (e: Exception) {\n                logE(\"Failed to remove library entry.\", e)\n                false\n            }\n            if (isDeleted) {\n                _libraryEntryWithModification.postValue(null)\n            } else {\n                acceptInternalAction(InternalAction.DeleteLibraryEntryFailed)\n            }\n        }\n    }\n\n    fun toggleFavorite() {\n        val favorite = favorite.value\n\n        viewModelScope.launch(Dispatchers.IO) {\n            if (favorite == null) {\n                val mediaItem = mediaModel.value ?: return@launch\n                val userId = getLocalUserId() ?: return@launch\n\n                try {\n                    val newFavorite = favoriteRepository.createMediaFavorite(userId, mediaItem.mediaType, mediaItem.id)\n                    _favorite.postValue(newFavorite)\n                } catch (e: Exception) {\n                    logE(\"Failed to create new favorite.\", e)\n                }\n            } else {\n                val favoriteId = favorite.id\n                try {\n                    val isSuccessful = favoriteRepository.deleteFavorite(favoriteId)\n                    if (isSuccessful) {\n                        _favorite.postValue(null)\n                    } else {\n                        throw ResourceUpdateFailed()\n                    }\n                } catch (e: Exception) {\n                    logE(\"Failed to delete favorite.\", e)\n                }\n            }\n        }\n    }\n}\n\nsealed class LibraryChangeResult {\n    data class LibraryUpdateResult(val result: LibraryEntryUpdateResult) : LibraryChangeResult()\n    data object AddNewLibraryEntryFailed : LibraryChangeResult()\n    data object DeleteLibraryEntryFailed : LibraryChangeResult()\n}\n\nprivate sealed class InternalAction {\n    data class LibraryUpdateResult(val result: LibraryEntryUpdateResult) : InternalAction()\n    data object AddNewLibraryEntryFailed : InternalAction()\n    data object DeleteLibraryEntryFailed : InternalAction()\n}\n\nsealed class MediaMappingsSate {\n    data object Initial : MediaMappingsSate()\n    data object Loading : MediaMappingsSate()\n    data class Success(val mappings: List<Mapping>) : MediaMappingsSate()\n    data class Error(val message: String) : MediaMappingsSate()\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/ManageLibraryBottomSheet.kt",
    "content": "package io.github.drumber.kitsune.ui.details\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.os.bundleOf\nimport androidx.fragment.app.setFragmentResult\nimport com.google.android.material.bottomsheet.BottomSheetDialogFragment\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.databinding.SheetManageLibraryBinding\n\nclass ManageLibraryBottomSheet : BottomSheetDialogFragment() {\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        val binding = SheetManageLibraryBinding.inflate(inflater, container, false)\n        binding.apply {\n            instance = this@ManageLibraryBottomSheet\n            title = arguments?.getString(BUNDLE_TITLE)\n        }\n        return binding.root\n    }\n\n    fun onStatusClicked(status: LibraryStatus) {\n        setFragmentResult(STATUS_REQUEST_KEY, bundleOf(BUNDLE_STATUS to status))\n        dismiss()\n    }\n\n    fun onRemoveClicked() {\n        setFragmentResult(REMOVE_REQUEST_KEY, bundleOf(BUNDLE_EXISTS_IN_LIBRARY to false))\n        dismiss()\n    }\n\n    fun existsInLibrary(): Boolean {\n        return arguments?.getBoolean(BUNDLE_EXISTS_IN_LIBRARY) ?: false\n    }\n\n    fun isAnime() = arguments?.getBoolean(BUNDLE_IS_ANIME) == true\n\n    companion object {\n        const val TAG = \"manage_library_bottom_sheet\"\n        const val BUNDLE_TITLE = \"title_bundle_key\"\n        const val BUNDLE_STATUS = \"status_bundle_key\"\n        const val BUNDLE_EXISTS_IN_LIBRARY = \"status_exists_in_library\"\n        const val BUNDLE_IS_ANIME = \"is_anime_key\"\n        const val STATUS_REQUEST_KEY = \"status_request_key\"\n        const val REMOVE_REQUEST_KEY = \"remove_request_key\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/MediaMappingsBottomSheet.kt",
    "content": "package io.github.drumber.kitsune.ui.details\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.lifecycle.lifecycleScope\nimport com.google.android.material.bottomsheet.BottomSheetDialogFragment\nimport io.github.drumber.kitsune.data.presentation.model.mapping.getExternalUrl\nimport io.github.drumber.kitsune.databinding.SheetMediaMappingsBinding\nimport io.github.drumber.kitsune.ui.adapter.MediaMappingsAdapter\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass MediaMappingsBottomSheet : BottomSheetDialogFragment() {\n\n    private var _binding: SheetMediaMappingsBinding? = null\n    private val binding get() = _binding!!\n    private val viewModel: DetailsViewModel by viewModel(ownerProducer = { requireParentFragment() })\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = SheetMediaMappingsBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        val adapter = MediaMappingsAdapter(requireContext(), mutableListOf())\n        binding.listMediaMappings.adapter = adapter\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.mappingsSate.collectLatest { state ->\n                binding.progressBarMediaMappings.isVisible = state is MediaMappingsSate.Loading\n                binding.tvErrorMediaMappings.isVisible = state is MediaMappingsSate.Error\n                binding.listMediaMappings.isVisible = state is MediaMappingsSate.Success\n\n                if (state is MediaMappingsSate.Success) {\n                    val mappings = state.mappings\n                        .distinctBy { it.getExternalUrl() ?: it.externalSite }\n                        .sortedBy { it.externalSite }\n                    adapter.dataSource.clear()\n                    adapter.dataSource.addAll(mappings)\n                    adapter.notifyDataSetChanged()\n                }\n            }\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        _binding = null\n    }\n\n    companion object {\n        const val TAG = \"media_mappings_bottom_sheet\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/characters/CharacterDetailsBottomSheet.kt",
    "content": "package io.github.drumber.kitsune.ui.details.characters\n\nimport android.graphics.drawable.Drawable\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.TooltipCompat\nimport androidx.core.view.isVisible\nimport androidx.lifecycle.lifecycleScope\nimport androidx.navigation.fragment.findNavController\nimport androidx.navigation.fragment.navArgs\nimport androidx.vectordrawable.graphics.drawable.Animatable2Compat.AnimationCallback\nimport androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.load.resource.bitmap.RoundedCorners\nimport com.google.android.material.bottomsheet.BottomSheetDialogFragment\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.addTransform\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.presentation.dto.toCharacter\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaDto\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.presentation.model.character.MediaCharacter\nimport io.github.drumber.kitsune.databinding.ItemDetailsInfoRowBinding\nimport io.github.drumber.kitsune.databinding.SheetCharacterDetailsBinding\nimport io.github.drumber.kitsune.ui.adapter.MediaCharacterAdapter\nimport io.github.drumber.kitsune.util.DataUtil.mapLanguageCodesToDisplayName\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.extensions.openCharacterOnMAL\nimport io.github.drumber.kitsune.util.extensions.openPhotoViewActivity\nimport io.github.drumber.kitsune.util.extensions.toPx\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\nimport java.util.concurrent.CopyOnWriteArrayList\n\nclass CharacterDetailsBottomSheet : BottomSheetDialogFragment() {\n\n    private var _binding: SheetCharacterDetailsBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: CharacterDetailsViewModel by viewModel()\n\n    private val navArgs by navArgs<CharacterDetailsBottomSheetArgs>()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = SheetCharacterDetailsBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        binding.rvMediaCharacters.adapter = MediaCharacterAdapter(\n            CopyOnWriteArrayList(),\n            Glide.with(this)\n        ) { _, mediaCharacter ->\n            val media = mediaCharacter.media\n                ?: return@MediaCharacterAdapter\n            val action =\n                CharacterDetailsBottomSheetDirections.actionCharacterDetailsBottomSheetToDetailsFragment(\n                    media.toMediaDto()\n                )\n            findNavController().navigateSafe(R.id.characterDetailsBottomSheet, action)\n        }\n\n        viewModel.initCharacter(navArgs.character.toCharacter())\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.characterFlow.collectLatest { character ->\n                updateCharacterData(character)\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.uiState.collectLatest { state ->\n                val isLoading = state.isLoadingMediaCharacters\n                binding.progressBar.isVisible = isLoading\n                binding.loadingWrapper.isVisible = isLoading || !state.hasMediaCharacters\n                binding.tvNoData.isVisible = !isLoading && !state.hasMediaCharacters\n                binding.rvMediaCharacters.isVisible = !isLoading && state.hasMediaCharacters\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.favoriteFlow.collectLatest { favorite ->\n                val icon = binding.btnFavorite.icon\n                if (favorite != null && icon is AnimatedVectorDrawableCompat) {\n                    icon.registerAnimationCallback(object : AnimationCallback() {\n                        override fun onAnimationEnd(drawable: Drawable?) {\n                            // binding can be null if the fragment is destroyed\n                            _binding?.btnFavorite?.setIconResource(R.drawable.ic_favorite_24)\n                        }\n                    })\n                } else {\n                    binding.btnFavorite.setIconResource(\n                        if (favorite != null) R.drawable.ic_favorite_24\n                        else R.drawable.ic_favorite_border_24\n                    )\n                }\n\n                TooltipCompat.setTooltipText(\n                    binding.btnFavorite,\n                    getString(\n                        if (favorite != null) R.string.action_remove_from_favorites\n                        else R.string.action_add_to_favorites\n                    )\n                )\n            }\n        }\n\n        binding.ivCharacter.setOnClickListener {\n            val fullCharacter = viewModel.characterFlow.replayCache.lastOrNull()\n                ?: return@setOnClickListener\n\n            fullCharacter.image?.originalOrDown()?.let { imageUrl ->\n                openPhotoViewActivity(\n                    imageUrl,\n                    fullCharacter.name,\n                    fullCharacter.image.smallOrHigher(),\n                    binding.ivCharacter\n                )\n            }\n        }\n\n        binding.btnOpenOnMal.setOnClickListener {\n            viewModel.characterFlow.replayCache.lastOrNull()?.malId?.let { malId ->\n                openCharacterOnMAL(malId)\n            }\n        }\n\n        binding.btnFavorite.setOnClickListener {\n            val addToFavorite = viewModel.toggleFavorite()\n            if (addToFavorite) {\n                AnimatedVectorDrawableCompat.create(requireContext(), R.drawable.animated_favorite)\n                    ?.apply {\n                        binding.btnFavorite.icon = this\n                        registerAnimationCallback(object : AnimationCallback() {\n                            var originalTintColor = binding.btnFavorite.iconTint\n                            override fun onAnimationStart(drawable: Drawable?) {\n                                drawable?.setTintList(null)\n                            }\n\n                            override fun onAnimationEnd(drawable: Drawable?) {\n                                drawable?.setTintList(originalTintColor)\n                            }\n                        })\n                        start()\n                    }\n            }\n            findNavController().previousBackStackEntry\n                ?.takeIf { it.destination.id == R.id.profile_fragment }\n                ?.savedStateHandle\n                ?.set(\"refreshFavorites\", true)\n        }\n    }\n\n    private fun updateCharacterData(character: Character) {\n        binding.character = character\n        updateNamesInTable(character.names)\n        updateMediaCharactersRecyclerView(character.mediaCharacters)\n        binding.btnOpenOnMal.isVisible = character.malId != null\n\n        Glide.with(this)\n            .load(character.image?.originalOrDown())\n            .fitCenter()\n            .addTransform(RoundedCorners(8.toPx()))\n            .placeholder(R.drawable.ic_insert_photo_48)\n            .into(binding.ivCharacter)\n    }\n\n    private fun updateNamesInTable(names: Titles?) {\n        binding.tableNames.removeViews(0, binding.tableNames.childCount - 1)\n\n        val sortedNames = names?.filterValues { !it.isNullOrBlank() }\n            ?.mapLanguageCodesToDisplayName(false)\n            ?.toList()\n            ?.sortedByDescending { it.first }\n\n        sortedNames?.forEach { (language, name) ->\n            val rowBinding = ItemDetailsInfoRowBinding.inflate(layoutInflater)\n            rowBinding.title = language\n            rowBinding.value = name\n            binding.tableNames.addView(rowBinding.root, 0)\n        }\n    }\n\n    private fun updateMediaCharactersRecyclerView(mediaCharacters: List<MediaCharacter>?) {\n        (binding.rvMediaCharacters.adapter as MediaCharacterAdapter).apply {\n            dataSet.clear()\n            val sortedMediaCharacters = mediaCharacters?.sortedBy { it.role?.ordinal }\n                ?: emptyList()\n            dataSet.addAll(sortedMediaCharacters)\n            notifyDataSetChanged()\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        _binding = null\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/characters/CharacterDetailsViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.details.characters\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.constants.Defaults\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.presentation.model.user.Favorite\nimport io.github.drumber.kitsune.data.repository.CharacterRepository\nimport io.github.drumber.kitsune.data.repository.FavoriteRepository\nimport io.github.drumber.kitsune.domain.user.GetLocalUserIdUseCase\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\n\nclass CharacterDetailsViewModel(\n    private val characterRepository: CharacterRepository,\n    private val getLocalUserId: GetLocalUserIdUseCase,\n    private val favoriteRepository: FavoriteRepository\n) : ViewModel() {\n\n    private val _characterFlow = MutableSharedFlow<Character>(\n        replay = 1,\n        onBufferOverflow = BufferOverflow.DROP_OLDEST\n    )\n    val characterFlow\n        get() = _characterFlow.asSharedFlow()\n\n    private val _favoriteFlow = MutableSharedFlow<Favorite?>(\n        replay = 1,\n        onBufferOverflow = BufferOverflow.DROP_OLDEST\n    )\n    val favoriteFlow\n        get() = _favoriteFlow.asSharedFlow()\n\n    private val _uiState = MutableStateFlow(UiState())\n    val uiState\n        get() = _uiState.asStateFlow()\n\n    fun initCharacter(character: Character) {\n        if (_characterFlow.replayCache.isNotEmpty() && _characterFlow.replayCache.firstOrNull()?.id == character.id) return\n\n        viewModelScope.launch {\n            _characterFlow.emit(character)\n\n            launch(Dispatchers.IO) fetchFavorite@{\n                val characterId = character.id\n                val favorite = fetchFavorite(characterId)\n                _favoriteFlow.emit(favorite)\n            }\n\n            launch(Dispatchers.IO) fetchFullCharacter@{\n                // fetch full character model\n                val characterId = character.id\n                _uiState.emit(UiState(isLoadingMediaCharacters = true))\n                val fullCharacter = try {\n                    fetchCharacterData(characterId)\n                } finally {\n                    _uiState.emit(UiState(isLoadingMediaCharacters = false))\n                }\n                if (fullCharacter != null) {\n                    _characterFlow.emit(fullCharacter)\n                }\n                _uiState.emit(UiState(hasMediaCharacters = fullCharacter?.mediaCharacters?.isNotEmpty() == true))\n            }\n        }\n    }\n\n    private suspend fun fetchCharacterData(id: String): Character? {\n        val filter = Filter()\n            .include(\"mediaCharacters\", \"mediaCharacters.media\")\n            .fields(\"media\", *Defaults.MINIMUM_COLLECTION_FIELDS)\n\n        return try {\n            characterRepository.getCharacter(id, filter)\n        } catch (e: Exception) {\n            logE(\"Failed to fetch character data.\", e)\n            null\n        }\n    }\n\n    private suspend fun fetchFavorite(characterId: String): Favorite? {\n        val userId = getLocalUserId() ?: return null\n\n        val filter = Filter()\n            .filter(\"user_id\", userId)\n            .filter(\"item_id\", characterId)\n            .filter(\"item_type\", \"Character\")\n\n        return try {\n            favoriteRepository.getAllFavorites(filter)?.firstOrNull()\n        } catch (e: Exception) {\n            logE(\"Failed to fetch favorites.\", e)\n            null\n        }\n    }\n\n    fun toggleFavorite(): Boolean {\n        val characterId = characterFlow.replayCache.firstOrNull()?.id ?: return false\n        val userId = getLocalUserId() ?: return false\n        val favorite = favoriteFlow.replayCache.firstOrNull()\n\n        viewModelScope.launch(Dispatchers.IO) {\n            if (favorite == null) {\n                val addedFavorite = addToFavorites(userId, characterId)\n                    ?: return@launch _favoriteFlow.emit(null)\n                _favoriteFlow.emit(addedFavorite)\n            } else {\n                val favoriteId = favorite.id\n                if (removeFromFavorites(favoriteId)) {\n                    _favoriteFlow.emit(null)\n                }\n            }\n        }\n        return favorite == null\n    }\n\n    private suspend fun addToFavorites(userId: String, characterId: String): Favorite? {\n        return try {\n            favoriteRepository.createCharacterFavorite(userId, characterId)\n        } catch (e: Exception) {\n            logE(\"Failed to post favorite.\", e)\n            null\n        }\n    }\n\n    private suspend fun removeFromFavorites(favoriteId: String): Boolean {\n        return try {\n            favoriteRepository.deleteFavorite(favoriteId)\n        } catch (e: Exception) {\n            logE(\"Failed to delete favorite.\", e)\n            false\n        }\n    }\n\n}\n\ndata class UiState(\n    val isLoadingMediaCharacters: Boolean = false,\n    val hasMediaCharacters: Boolean = false\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/characters/CharacterFilterAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.details.characters\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport io.github.drumber.kitsune.databinding.ItemCharacterFilterBinding\n\nclass CharacterFilterAdapter(\n    isViewHolderVisible: Boolean,\n    var languages: List<String> = emptyList(),\n    var selectedLanguage: String? = null,\n    private val onItemClicked: (language: String) -> Unit\n) : RecyclerView.Adapter<CharacterFilterAdapter.CharacterFilterViewHolder>() {\n\n    var isViewHolderVisible = isViewHolderVisible\n        set(value) {\n            val previous = field\n            field = value\n            when {\n                previous != value && value -> notifyItemInserted(0)\n                previous != value && !value -> notifyItemRemoved(0)\n            }\n        }\n\n    fun notifyItemChanged() = notifyItemChanged(0)\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterFilterViewHolder {\n        return CharacterFilterViewHolder(\n            ItemCharacterFilterBinding.inflate(\n                LayoutInflater.from(parent.context),\n                parent,\n                false\n            )\n        )\n    }\n\n    override fun onBindViewHolder(holder: CharacterFilterViewHolder, position: Int) {\n        holder.setLanguages(languages)\n        holder.setSelectedLanguage(selectedLanguage)\n    }\n\n    override fun getItemCount() = if (isViewHolderVisible) 1 else 0\n\n    inner class CharacterFilterViewHolder(val binding: ItemCharacterFilterBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n        init {\n            binding.chipLanguage.setOnClickListener {\n                val languages = languages\n                val checkedItemIndex = languages.indexOf(selectedLanguage)\n                MaterialAlertDialogBuilder(binding.root.context)\n                    .setSingleChoiceItems(languages.toTypedArray(), checkedItemIndex) { dialog, i ->\n                        if (i == checkedItemIndex) {\n                            dialog.dismiss()\n                            return@setSingleChoiceItems\n                        }\n                        val checkedLanguage = languages[i]\n                        setSelectedLanguage(checkedLanguage)\n                        selectedLanguage = checkedLanguage\n                        onItemClicked(checkedLanguage)\n                        dialog.dismiss()\n                    }\n                    .show()\n            }\n            setLanguages(languages)\n            setSelectedLanguage(selectedLanguage)\n        }\n\n        fun setLanguages(languages: List<String>) {\n            binding.chipLanguage.apply {\n                if (!languages.contains(text)) {\n                    text = languages.firstOrNull()\n                }\n            }\n        }\n\n        fun setSelectedLanguage(language: String?) {\n            binding.chipLanguage.text = language\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/characters/CharactersFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.details.characters\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport androidx.navigation.fragment.findNavController\nimport androidx.navigation.fragment.navArgs\nimport androidx.recyclerview.widget.ConcatAdapter\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.Glide\nimport com.google.android.material.navigation.NavigationBarView\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.dto.toCharacterDto\nimport io.github.drumber.kitsune.databinding.FragmentCharactersBinding\nimport io.github.drumber.kitsune.ui.adapter.paging.CharacterPagingAdapter\nimport io.github.drumber.kitsune.ui.adapter.paging.ResourceLoadStateAdapter\nimport io.github.drumber.kitsune.ui.component.updateLoadState\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass CharactersFragment : Fragment(R.layout.fragment_characters),\n    NavigationBarView.OnItemReselectedListener {\n\n    private val args: CharactersFragmentArgs by navArgs()\n\n    private var _binding: FragmentCharactersBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: CharactersViewModel by viewModel()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentCharactersBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        viewModel.setMediaId(args.mediaId, args.isAnime)\n\n        binding.apply {\n            collapsingToolbar.initWindowInsetsListener(consume = false)\n            toolbar.initWindowInsetsListener(false)\n            toolbar.setNavigationOnClickListener { findNavController().navigateUp() }\n\n            rvMedia.initPaddingWindowInsetsListener(\n                left = true,\n                right = true,\n                bottom = true,\n                consume = false\n            )\n        }\n\n        binding.layoutLoading.btnRetry.setOnClickListener {\n            viewModel.retry(args.mediaId, args.isAnime)\n        }\n\n        val filterAdapter = CharacterFilterAdapter(false) { language ->\n            viewModel.setLanguage(language)\n        }\n        val pagingAdapter = CharacterPagingAdapter(Glide.with(this)) { _, character ->\n            val action = CharactersFragmentDirections\n                .actionCharactersFragmentToCharacterDetailsBottomSheet(character.toCharacterDto())\n            findNavController().navigateSafe(R.id.characters_fragment, action)\n        }\n        val concatAdapter = ConcatAdapter(\n            filterAdapter,\n            pagingAdapter.withLoadStateHeaderAndFooter(\n                header = ResourceLoadStateAdapter(pagingAdapter),\n                footer = ResourceLoadStateAdapter(pagingAdapter)\n            )\n        )\n        binding.rvMedia.adapter = concatAdapter\n        binding.rvMedia.layoutManager =\n            LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                pagingAdapter.loadStateFlow.collectLatest { loadState ->\n                    binding.layoutLoading.updateLoadState(\n                        binding.rvMedia,\n                        pagingAdapter.itemCount,\n                        loadState\n                    )\n                }\n            }\n        }\n\n        viewModel.languages.observe(viewLifecycleOwner) { languages ->\n            filterAdapter.isViewHolderVisible = languages.isNotEmpty()\n            filterAdapter.languages = languages\n            filterAdapter.selectedLanguage = viewModel.selectedLanguage\n            filterAdapter.notifyItemChanged()\n        }\n\n        viewModel.isLoadingLanguages.observe(viewLifecycleOwner) { isLoading ->\n            binding.layoutLoading.apply {\n                root.isVisible = isVisible\n                progressBar.isVisible = isLoading\n                tvError.isVisible = false\n                btnRetry.isVisible = false\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.dataSource.collectLatest { data ->\n                    pagingAdapter.submitData(data)\n                }\n            }\n        }\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        if (binding.rvMedia.canScrollVertically(-1)) {\n            binding.rvMedia.smoothScrollToPosition(0)\n            binding.appBarLayout.setExpanded(true)\n        } else {\n            findNavController().navigateUp()\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/characters/CharactersViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.details.characters\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.asFlow\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.PagingData\nimport androidx.paging.cachedIn\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.presentation.model.media.production.Casting\nimport io.github.drumber.kitsune.data.repository.AnimeRepository\nimport io.github.drumber.kitsune.data.repository.CastingRepository\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nclass CharactersViewModel(\n    private val castingRepository: CastingRepository,\n    private val animeRepository: AnimeRepository\n) : ViewModel() {\n\n    private val filter = MutableLiveData<Filter>()\n\n    private var mediaId: String? = null\n\n    private val _languages = MutableLiveData<List<String>>()\n    val languages: LiveData<List<String>>\n        get() = _languages\n\n    var selectedLanguage: String? = null\n        private set\n\n    private val _isLoadingLanguages = MutableLiveData(false)\n    val isLoadingLanguages: LiveData<Boolean>\n        get() = _isLoadingLanguages\n\n    fun setMediaId(id: String, isAnime: Boolean) {\n        if (id == mediaId) return\n        mediaId = id\n\n        if (isAnime) {\n            // fetch all languages of the anime\n            viewModelScope.launch(Dispatchers.IO) {\n                val langs = fetchLanguages(id) ?: emptyList()\n                // select Japanese, English or the first language in the list\n                selectedLanguage = langs.find { it == \"Japanese\" }\n                    ?: langs.find { it == \"English\" } ?: langs.firstOrNull()\n\n                withContext(Dispatchers.Main) {\n                    _languages.value = langs\n                    updateFilter()\n                }\n            }\n        } else {\n            updateFilter()\n        }\n    }\n\n    fun retry(id: String, isAnime: Boolean) {\n        mediaId = null\n        setMediaId(id, isAnime)\n    }\n\n    fun setLanguage(language: String) {\n        if (languages.value?.contains(language) == true) {\n            selectedLanguage = language\n            updateFilter()\n        }\n    }\n\n    private fun updateFilter() {\n        val id = mediaId ?: return\n\n        val filter = Filter()\n            .filter(\"media_id\", id)\n            .filter(\"is_character\", \"true\")\n            .include(\"character\", \"person\")\n            .sort(\"-featured\")\n        selectedLanguage?.let { filter.filter(\"language\", it) }\n\n        this.filter.value = filter\n    }\n\n    private suspend fun fetchLanguages(id: String): List<String>? {\n        _isLoadingLanguages.postValue(true)\n        return try {\n            animeRepository.getLanguages(id)\n        } catch (e: Exception) {\n            logE(\"Failed to fetch languages for anime with id '$id'.\", e)\n            null\n        } finally {\n            _isLoadingLanguages.postValue(false)\n        }\n    }\n\n    val dataSource: Flow<PagingData<Casting>> = filter.asFlow().flatMapLatest { filter ->\n        castingRepository.castingPager(filter, Kitsu.DEFAULT_PAGE_SIZE)\n    }.cachedIn(viewModelScope)\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/episodes/EpisodesFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.details.episodes\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.os.bundleOf\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport androidx.navigation.fragment.findNavController\nimport androidx.navigation.fragment.navArgs\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.Glide\nimport com.google.android.material.navigation.NavigationBarView\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.presentation.dto.toMedia\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaUnitDto\nimport io.github.drumber.kitsune.data.presentation.model.media.unit.MediaUnit\nimport io.github.drumber.kitsune.databinding.FragmentMediaListBinding\nimport io.github.drumber.kitsune.ui.adapter.paging.MediaUnitPagingAdapter\nimport io.github.drumber.kitsune.ui.adapter.paging.ResourceLoadStateAdapter\nimport io.github.drumber.kitsune.ui.component.updateLoadState\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.showSnackbarOnFailure\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass EpisodesFragment : Fragment(R.layout.fragment_media_list),\n    MediaUnitPagingAdapter.MediaUnitActionListener,\n    NavigationBarView.OnItemReselectedListener {\n\n    private val args: EpisodesFragmentArgs by navArgs()\n\n    private var _binding: FragmentMediaListBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: EpisodesViewModel by viewModel()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentMediaListBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        viewModel.setMedia(args.media.toMedia())\n\n        binding.collapsingToolbar.initWindowInsetsListener(consume = false)\n        binding.toolbar.apply {\n            initWindowInsetsListener(consume = false)\n            title = getString(\n                when (args.media.type) {\n                    MediaType.Anime -> R.string.title_episodes\n                    MediaType.Manga -> R.string.title_chapters\n                }\n            )\n            setNavigationOnClickListener { findNavController().navigateUp() }\n        }\n\n        binding.rvMedia.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            bottom = true,\n            consume = false\n        )\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.libraryUpdateResultFlow.collectLatest {\n                    it.showSnackbarOnFailure(binding.rvMedia)\n                }\n            }\n        }\n\n        val adapter = MediaUnitPagingAdapter(\n            Glide.with(this),\n            args.media.toMedia().posterImageUrl,\n            viewModel.libraryEntryWrapper.value != null,\n            this\n        )\n        binding.rvMedia.adapter = adapter.withLoadStateHeaderAndFooter(\n            header = ResourceLoadStateAdapter(adapter),\n            footer = ResourceLoadStateAdapter(adapter)\n        )\n        binding.rvMedia.layoutManager =\n            LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)\n\n        binding.layoutLoading.btnRetry.setOnClickListener { adapter.retry() }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                adapter.loadStateFlow.collectLatest { loadState ->\n                    binding.layoutLoading.updateLoadState(\n                        binding.rvMedia,\n                        adapter.itemCount,\n                        loadState\n                    )\n                }\n            }\n        }\n\n        viewModel.libraryEntryWrapper.observe(viewLifecycleOwner) {\n            adapter.setIsWatchedCheckboxEnabled(it != null)\n            it?.progress?.let { progress ->\n                adapter.updateLibraryWatchCount(progress)\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.dataSource.collectLatest { data ->\n                    adapter.submitData(data)\n                }\n            }\n        }\n    }\n\n    private fun showDetailsBottomSheet(mediaUnit: MediaUnit) {\n        val sheetMediaUnit = MediaUnitDetailsBottomSheet()\n        sheetMediaUnit.arguments = bundleOf(\n            MediaUnitDetailsBottomSheet.BUNDLE_MEDIA_UNIT_ADAPTER to mediaUnit.toMediaUnitDto(),\n            MediaUnitDetailsBottomSheet.BUNDLE_THUMBNAIL to args.media.toMedia().posterImageUrl\n        )\n        sheetMediaUnit.show(parentFragmentManager, MediaUnitDetailsBottomSheet.TAG)\n    }\n\n    override fun onMediaUnitClicked(mediaUnit: MediaUnit) {\n        showDetailsBottomSheet(mediaUnit)\n    }\n\n    override fun onWatchStateChanged(mediaUnit: MediaUnit, isWatched: Boolean) {\n        viewModel.setMediaUnitWatched(mediaUnit, isWatched)\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        if (binding.rvMedia.canScrollVertically(-1)) {\n            binding.rvMedia.smoothScrollToPosition(0)\n            binding.appBarLayout.setExpanded(true)\n        } else {\n            findNavController().navigateUp()\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/episodes/EpisodesViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.details.episodes\n\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.asFlow\nimport androidx.lifecycle.switchMap\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.PagingData\nimport androidx.paging.cachedIn\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.presentation.model.media.unit.MediaUnit\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.data.repository.MediaUnitRepository\nimport io.github.drumber.kitsune.data.repository.MediaUnitRepository.MediaUnitType\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryProgressUseCase\nimport io.github.drumber.kitsune.domain.user.GetLocalUserIdUseCase\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.launch\n\nclass EpisodesViewModel(\n    private val mediaUnitRepository: MediaUnitRepository,\n    private val libraryRepository: LibraryRepository,\n    private val updateLibraryEntryProgress: UpdateLibraryEntryProgressUseCase,\n    private val getLocalUserId: GetLocalUserIdUseCase\n) : ViewModel() {\n\n    private val acceptLibraryUpdateResult: (LibraryEntryUpdateResult) -> Unit\n\n    val libraryUpdateResultFlow: Flow<LibraryEntryUpdateResult>\n\n    private val media = MutableLiveData<Media>()\n\n    val libraryEntryWrapper = media.switchMap { media ->\n        val dbEntry = libraryRepository.getLibraryEntryWithModificationFromMediaAsLiveData(media.id)\n        val userId = getLocalUserId()\n        if (dbEntry.value == null && userId != null) {\n            viewModelScope.launch(Dispatchers.IO) {\n                try {\n                    libraryRepository.fetchAndStoreLibraryEntryForMedia(userId, media)\n                } catch (e: Exception) {\n                    logE(\"Failed to fetch library entry for media: ${media.id}\", e)\n                }\n            }\n        }\n        return@switchMap dbEntry\n    }\n\n    init {\n        val mutableLibraryUpdateResultFlow = MutableSharedFlow<LibraryEntryUpdateResult>()\n        libraryUpdateResultFlow = mutableLibraryUpdateResultFlow.asSharedFlow()\n\n        acceptLibraryUpdateResult = {\n            viewModelScope.launch { mutableLibraryUpdateResultFlow.emit(it) }\n        }\n    }\n\n    fun setMedia(media: Media) {\n        if (media != this.media.value) {\n            this.media.value = media\n        }\n    }\n\n    fun setMediaUnitWatched(mediaUnit: MediaUnit, isWatched: Boolean) {\n        val libraryEntry = libraryEntryWrapper.value?.libraryEntry ?: return\n        val number = mediaUnit.number ?: 0\n        val progress = if (isWatched) {\n            number\n        } else {\n            number.minus(1).coerceAtLeast(0)\n        }\n\n        viewModelScope.launch(Dispatchers.IO) {\n            val updateResult = updateLibraryEntryProgress(libraryEntry, progress)\n            acceptLibraryUpdateResult(updateResult)\n        }\n    }\n\n    val dataSource: Flow<PagingData<MediaUnit>> = media.asFlow().flatMapLatest { media ->\n        val filter = Filter()\n            .sort(\"number\")\n        val type = when (media) {\n            is Anime -> {\n                filter.filter(\"media_id\", media.id)\n                MediaUnitType.EPISODE\n            }\n\n            is Manga -> {\n                filter.filter(\"manga_id\", media.id)\n                MediaUnitType.CHAPTER\n            }\n        }\n        mediaUnitRepository.mediaUnitPager(type, filter, Kitsu.DEFAULT_PAGE_SIZE)\n    }.cachedIn(viewModelScope)\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/details/episodes/MediaUnitDetailsBottomSheet.kt",
    "content": "package io.github.drumber.kitsune.ui.details.episodes\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport com.bumptech.glide.Glide\nimport com.google.android.material.bottomsheet.BottomSheetDialogFragment\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.dto.MediaUnitDto\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaUnit\nimport io.github.drumber.kitsune.databinding.SheetMediaUnitDetailsBinding\nimport io.github.drumber.kitsune.util.extensions.openPhotoViewActivity\n\nclass MediaUnitDetailsBottomSheet : BottomSheetDialogFragment() {\n\n    private var _binding: SheetMediaUnitDetailsBinding? = null\n    private val binding get() = _binding!!\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = SheetMediaUnitDetailsBinding.inflate(inflater, container, false)\n        val mediaUnitDto: MediaUnitDto? = arguments?.getParcelable(BUNDLE_MEDIA_UNIT_ADAPTER)\n        val mediaUnit = mediaUnitDto?.toMediaUnit()\n        binding.mediaUnit = mediaUnit\n\n        val thumbnailUrl = mediaUnit?.thumbnail?.smallOrHigher() ?: arguments?.getString(BUNDLE_THUMBNAIL)\n        Glide.with(this)\n            .load(thumbnailUrl)\n            .centerCrop()\n            .placeholder(R.drawable.ic_insert_photo_48)\n            .into(binding.ivThumbnail)\n\n        binding.ivThumbnail.setOnClickListener {\n            mediaUnit?.thumbnail?.originalOrDown()?.let { imageUrl ->\n                val title = mediaUnit.title(requireContext())\n                openPhotoViewActivity(imageUrl, title, thumbnailUrl)\n            }\n        }\n\n        return binding.root\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        _binding = null\n    }\n\n    companion object {\n        const val TAG = \"media_unit_details_bottom_sheet\"\n        const val BUNDLE_MEDIA_UNIT_ADAPTER = \"media_unit_adapter_bundle_key\"\n        const val BUNDLE_THUMBNAIL = \"thumbnail_bundle_key\"\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/library/LibraryFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.library\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.ConnectivityManager\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.activity.OnBackPressedCallback\nimport androidx.activity.addCallback\nimport androidx.annotation.OptIn\nimport androidx.appcompat.widget.SearchView\nimport androidx.core.view.doOnPreDraw\nimport androidx.core.view.isEmpty\nimport androidx.core.view.isNotEmpty\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.setFragmentResultListener\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport androidx.navigation.fragment.FragmentNavigatorExtras\nimport androidx.navigation.fragment.findNavController\nimport androidx.paging.CombinedLoadStates\nimport androidx.paging.LoadState\nimport androidx.recyclerview.widget.GridLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.SimpleItemAnimator\nimport com.algolia.instantsearch.core.searcher.Debouncer\nimport com.bumptech.glide.Glide\nimport com.google.android.material.appbar.AppBarLayout\nimport com.google.android.material.badge.BadgeDrawable\nimport com.google.android.material.badge.BadgeUtils\nimport com.google.android.material.badge.ExperimentalBadgeUtils\nimport com.google.android.material.chip.Chip\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.navigation.NavigationBarView\nimport com.google.android.material.shape.MaterialShapeDrawable\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.library.LibraryEntryKind\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaDto\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryUiModel\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.databinding.FragmentLibraryBinding\nimport io.github.drumber.kitsune.ui.adapter.paging.LibraryEntriesAdapter\nimport io.github.drumber.kitsune.ui.adapter.paging.ResourceLoadStateAdapter\nimport io.github.drumber.kitsune.ui.authentication.AuthenticationActivity\nimport io.github.drumber.kitsune.ui.base.BaseFragment\nimport io.github.drumber.kitsune.ui.component.ResponsiveGridLayoutManager\nimport io.github.drumber.kitsune.ui.component.updateLoadState\nimport io.github.drumber.kitsune.ui.library.LibraryChangeResult.LibrarySynchronizationResult\nimport io.github.drumber.kitsune.ui.library.LibraryChangeResult.LibraryUpdateResult\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.extensions.setAppTheme\nimport io.github.drumber.kitsune.util.extensions.toPx\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.showSnackbarOnAnyFailure\nimport io.github.drumber.kitsune.util.ui.showSnackbarOnFailure\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass LibraryFragment : BaseFragment(R.layout.fragment_library, true),\n    LibraryEntriesAdapter.LibraryEntryActionListener,\n    NavigationBarView.OnItemReselectedListener {\n\n    private var _binding: FragmentLibraryBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: LibraryViewModel by viewModel()\n\n    private var offlineLibraryModificationsAmount = 0\n    private lateinit var offlineLibraryUpdateBadge: BadgeDrawable\n\n    private var searchViewBackPressedCallback: OnBackPressedCallback? = null\n\n    private val searchDebouncer by lazy { Debouncer(300L) }\n\n    private val autoSyncDebouncer by lazy { Debouncer(5000L) }\n\n    companion object {\n        const val RESULT_KEY_RATING = \"library_rating_result_key\"\n        const val RESULT_KEY_REMOVE_RATING = \"library_remove_rating_result_key\"\n        const val RESULT_KEY_EDIT_ENTRY_UPDATED = \"library_edit_entry_updated\"\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        offlineLibraryUpdateBadge = BadgeDrawable.create(requireContext())\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentLibraryBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        postponeEnterTransition()\n\n        if (!viewModel.hasUser() || findNavController().currentBackStackEntry?.arguments == null) {\n            view.doOnPreDraw { startPostponedEnterTransition() }\n        } else {\n            // safeguard: ensure startPostponedEnterTransition() got called within 200ms\n            view.postDelayed({\n                startPostponedEnterTransition()\n            }, 200)\n        }\n\n        binding.apply {\n            appBarLayout.statusBarForeground =\n                MaterialShapeDrawable.createWithElevationOverlay(context)\n            toolbar.initPaddingWindowInsetsListener(\n                left = true,\n                right = true,\n                consume = false\n            )\n            scrollViewFilter.initPaddingWindowInsetsListener(\n                left = true,\n                right = true,\n                consume = false\n            )\n\n            swipeRefreshLayout.initPaddingWindowInsetsListener(\n                left = true,\n                right = true,\n                consume = false\n            )\n            layoutNotLoggedIn.initPaddingWindowInsetsListener(\n                left = true,\n                top = true,\n                right = true,\n                bottom = true,\n                consume = false\n            )\n\n            btnLogin.setOnClickListener {\n                val intent = Intent(requireActivity(), AuthenticationActivity::class.java)\n                startActivity(intent)\n            }\n        }\n        val initialToolbarScrollFlags =\n            (binding.toolbar.layoutParams as AppBarLayout.LayoutParams).scrollFlags\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.localUser.collectLatest { user ->\n                    val isLoggedIn = user != null\n                    binding.apply {\n                        initToolbarMenu(isVisible = isLoggedIn)\n                        rvLibraryEntries.isVisible = isLoggedIn\n                        nsvNotLoggedIn.isVisible = !isLoggedIn\n                        scrollViewFilter.isVisible = isLoggedIn\n                        // disable toolbar scrolling if library is not shown (not logged in)\n                        (toolbar.layoutParams as AppBarLayout.LayoutParams).scrollFlags =\n                            if (isLoggedIn) {\n                                initialToolbarScrollFlags\n                            } else {\n                                AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL\n                            }\n                    }\n                }\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.libraryChangeResultFlow.collectLatest {\n                    when (it) {\n                        is LibraryUpdateResult -> it.result.showSnackbarOnFailure(binding.rvLibraryEntries)\n                        is LibrarySynchronizationResult -> it.results.showSnackbarOnAnyFailure(\n                            binding.rvLibraryEntries\n                        )\n                    }\n                }\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.state\n                    .map { it.isLibraryUpdateOperationInProgress }\n                    .distinctUntilChanged()\n                    .collectLatest {\n                        binding.progressIndicator.visibility =\n                            if (it) View.VISIBLE else View.INVISIBLE\n                    }\n            }\n        }\n\n        setFragmentResultListener(RESULT_KEY_RATING) { _, bundle ->\n            val rating = bundle.getInt(RatingBottomSheet.BUNDLE_RATING, -1)\n            if (rating != -1) {\n                viewModel.updateRating(rating)\n            }\n        }\n\n        setFragmentResultListener(RESULT_KEY_REMOVE_RATING) { _, _ ->\n            viewModel.updateRating(null)\n        }\n\n        setFragmentResultListener(RESULT_KEY_EDIT_ENTRY_UPDATED) { _, _ ->\n            viewModel.triggerAdapterUpdate()\n        }\n\n        initToolbarMenu(isVisible = viewModel.hasUser())\n        initFilterChips()\n        initRecyclerView()\n    }\n\n    private fun initFilterChips() {\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.state\n                    .map { it.filter.kind }\n                    .distinctUntilChanged()\n                    .collectLatest { kind ->\n                        binding.chipMediaKind.setText(\n                            when (kind) {\n                                LibraryEntryKind.Anime -> R.string.anime\n                                LibraryEntryKind.Manga -> R.string.manga\n                                else -> R.string.library_kind_all\n                            }\n                        )\n\n                        binding.chipCurrent.setText(\n                            if (kind == LibraryEntryKind.Manga) R.string.library_status_reading\n                            else R.string.library_status_watching\n                        )\n                        binding.chipPlanned.setText(\n                            if (kind == LibraryEntryKind.Manga) R.string.library_status_planned_manga\n                            else R.string.library_status_planned\n                        )\n                    }\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.state\n                    .map { it.filter.libraryStatus }\n                    .distinctUntilChanged()\n                    .collectLatest { status ->\n                        binding.apply {\n                            chipCurrent.isChecked = status.contains(LibraryStatus.Current)\n                            chipPlanned.isChecked = status.contains(LibraryStatus.Planned)\n                            chipCompleted.isChecked = status.contains(LibraryStatus.Completed)\n                            chipOnHold.isChecked = status.contains(LibraryStatus.OnHold)\n                            chipDropped.isChecked = status.contains(LibraryStatus.Dropped)\n                        }\n                    }\n            }\n        }\n\n        binding.apply {\n            chipMediaKind.setOnClickListener { showMediaSelectorDialog() }\n            chipCurrent.initStatusClickListener(LibraryStatus.Current)\n            chipPlanned.initStatusClickListener(LibraryStatus.Planned)\n            chipCompleted.initStatusClickListener(LibraryStatus.Completed)\n            chipOnHold.initStatusClickListener(LibraryStatus.OnHold)\n            chipDropped.initStatusClickListener(LibraryStatus.Dropped)\n        }\n    }\n\n    private fun Chip.initStatusClickListener(status: LibraryStatus) {\n        setOnClickListener {\n            val statusList = viewModel.state.value.filter.libraryStatus.toMutableList()\n            if (statusList.contains(status)) {\n                statusList.remove(status)\n            } else {\n                statusList.add(status)\n            }\n            viewModel.setLibraryEntryStatus(statusList)\n        }\n    }\n\n    private fun showMediaSelectorDialog() {\n        val items = listOf(R.string.library_kind_all, R.string.anime, R.string.manga)\n            .map { getString(it) }.toTypedArray()\n        val prevSelected = viewModel.state.value.filter.kind.ordinal\n        MaterialAlertDialogBuilder(requireContext())\n            .setTitle(R.string.title_media_type)\n            .setSingleChoiceItems(items, prevSelected) { dialog, which ->\n                if (which != prevSelected) {\n                    val kind = LibraryEntryKind.entries[which]\n                    viewModel.setLibraryEntryKind(kind)\n                }\n                dialog.dismiss()\n            }\n            .show()\n    }\n\n    private fun initRecyclerView() {\n        val glide = Glide.with(this)\n        val adapter = LibraryEntriesAdapter(glide, this)\n\n        var lastLoadState: CombinedLoadStates? = null\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                adapter.loadStateFlow.collectLatest { state ->\n                    lastLoadState = state\n                    if (view?.parent == null) return@collectLatest\n\n                    if (!state.source.isIdle) {\n                        // start postponed transition immediate if loading from network\n                        view?.doOnPreDraw { startPostponedEnterTransition() }\n                    } else if (state.isIdle) {\n                        // else wait for adapter to idle\n                        view?.doOnPreDraw { startPostponedEnterTransition() }\n                    }\n\n                    val isSearching = viewModel.state.value.filter.searchQuery.isNotBlank()\n                    val isNotLoading = when {\n                        adapter.itemCount < 1 -> state.refresh is LoadState.NotLoading\n\n                        isSearching -> state.source.refresh is LoadState.NotLoading\n\n                        else -> state.mediator?.refresh !is LoadState.Loading\n                                || state.source.refresh is LoadState.NotLoading\n                    }\n\n                    binding.apply {\n                        layoutLoading.updateLoadState(\n                            rvLibraryEntries,\n                            adapter.itemCount,\n                            state,\n                            useRemoteMediator = true,\n                            checkIsNotLoading = { isNotLoading }\n                        )\n\n                        swipeRefreshLayout.isRefreshing =\n                            swipeRefreshLayout.isRefreshing && state.source.refresh is LoadState.Loading\n                    }\n\n                    // Check if a library entry was updated and try to scroll to the item position\n                    val scrollToEntryId = viewModel.scrollToUpdatedEntryId\n                    if (scrollToEntryId != null && state.isIdle) {\n                        val indexOfUpdatedEntry = adapter.snapshot()\n                            .indexOfFirst { (it as? LibraryEntryUiModel.EntryModel)?.entry?.id == scrollToEntryId }\n\n                        if (indexOfUpdatedEntry != -1) {\n                            binding.rvLibraryEntries.scrollToPosition(indexOfUpdatedEntry)\n                            viewModel.hasScrolledToUpdatedEntry()\n                        } else {\n                            // item was not found in paging snapshot, reset scrollToUpdatedEntryId\n                            viewModel.hasScrolledToUpdatedEntry()\n                        }\n                    }\n                }\n            }\n        }\n\n        binding.rvLibraryEntries.apply {\n            initPaddingWindowInsetsListener(bottom = true, consume = false)\n            this.adapter = adapter.withLoadStateHeaderAndFooter(\n                header = ResourceLoadStateAdapter(adapter),\n                footer = ResourceLoadStateAdapter(adapter)\n            )\n            layoutManager = ResponsiveGridLayoutManager(context, 350.toPx(), 1).apply {\n                spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {\n                    override fun getSpanSize(position: Int): Int {\n                        return if (adapter.getItemViewType(position) == R.layout.item_library_status_separator) {\n                            spanCount\n                        } else {\n                            1\n                        }\n                    }\n                }\n            }\n            // disable change animation to prevent \"blinking\"\n            (itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false\n\n            addOnScrollListener(object : RecyclerView.OnScrollListener() {\n                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {\n                    if (dy != 0) {\n                        val currentFilter = viewModel.state.value.filter\n                        viewModel.acceptAction(UiAction.Scroll(currentFilter))\n                    }\n                }\n            })\n\n            val notLoading = adapter.loadStateFlow\n                .map { it.isIdle }\n                .distinctUntilChanged()\n\n            val hasNotScrolledForCurrentFilter = viewModel.state\n                .map { it.hasNotScrolledForCurrentFilter }\n                .distinctUntilChanged()\n\n            val shouldScrollToTop = combine(\n                notLoading,\n                hasNotScrolledForCurrentFilter,\n                Boolean::and\n            ).distinctUntilChanged()\n\n            viewLifecycleOwner.lifecycleScope.launch {\n                viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                    shouldScrollToTop.collectLatest { shouldScroll ->\n                        if (shouldScroll) {\n                            scrollToPosition(0)\n                        }\n                    }\n                }\n            }\n        }\n\n        binding.swipeRefreshLayout.apply {\n            setAppTheme()\n            setOnRefreshListener {\n                if (offlineLibraryModificationsAmount > 0) {\n                    viewModel.synchronizeOfflineLibraryUpdates()\n                }\n                adapter.refresh()\n            }\n        }\n\n        viewModel.doRefreshListener = { adapter.refresh() }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.pagingDataFlow.collect {\n                    adapter.submitData(it)\n                }\n            }\n        }\n\n        viewModel.notSynchronizedLibraryEntryModifications.observe(viewLifecycleOwner) {\n            if (!viewModel.hasUser())\n                return@observe\n\n            viewModel.invalidatePagingSource()\n            offlineLibraryModificationsAmount = it.size\n            updateToolbarMenu(binding.toolbar.menu)\n\n            autoSyncDebouncer.debounce(viewLifecycleOwner.lifecycleScope) {\n                // synchronize library if there are offline library updates and network is not metered\n                val connectivityManager =\n                    requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n                if (it.isNotEmpty() && !connectivityManager.isActiveNetworkMetered) {\n                    viewModel.synchronizeOfflineLibraryUpdates()\n                }\n            }\n        }\n    }\n\n    override fun onItemClicked(view: View, item: LibraryEntryWithModification) {\n        val media = item.libraryEntry.media\n        if (media != null) {\n            val detailsTransitionName = getString(R.string.details_poster_transition_name)\n            val extras =\n                FragmentNavigatorExtras(view.findViewById<View>(R.id.iv_thumbnail) to detailsTransitionName)\n            val action =\n                LibraryFragmentDirections.actionLibraryFragmentToDetailsFragment(media.toMediaDto())\n            findNavController().navigateSafe(R.id.library_fragment, action, extras)\n        }\n    }\n\n    override fun onItemLongClicked(item: LibraryEntryWithModification) {\n        val action =\n            LibraryFragmentDirections.actionLibraryFragmentToLibraryEditEntryFragment(\n                item.libraryEntry.id,\n                RESULT_KEY_EDIT_ENTRY_UPDATED\n            )\n        findNavController().navigateSafe(R.id.library_fragment, action)\n    }\n\n    override fun onEpisodeWatchedClicked(item: LibraryEntryWithModification) {\n        viewModel.markEpisodeWatched(item)\n    }\n\n    override fun onEpisodeUnwatchedClicked(item: LibraryEntryWithModification) {\n        viewModel.markEpisodeUnwatched(item)\n    }\n\n    override fun onRatingClicked(item: LibraryEntryWithModification) {\n        viewModel.lastRatedLibraryEntry = item.libraryEntry\n\n        val action = LibraryFragmentDirections.actionLibraryFragmentToRatingBottomSheet(\n            title = item.media?.title ?: \"\",\n            ratingTwenty = item.ratingTwenty ?: -1,\n            ratingResultKey = RESULT_KEY_RATING,\n            removeResultKey = RESULT_KEY_REMOVE_RATING,\n            ratingSystem = RatingSystemUtil.getRatingSystem()\n        )\n        findNavController().navigateSafe(R.id.library_fragment, action)\n    }\n\n    private fun initToolbarMenu(isVisible: Boolean) {\n        val toolbar = binding.toolbar\n\n        if (isVisible && toolbar.menu.isNotEmpty()) return\n\n        toolbar.menu.clear()\n        searchViewBackPressedCallback?.remove()\n\n        if (!isVisible)\n            return\n\n        toolbar.inflateMenu(R.menu.library_menu)\n        toolbar.setOnMenuItemClickListener { item ->\n            return@setOnMenuItemClickListener if (item.itemId == R.id.menu_synchronize) {\n                viewModel.synchronizeOfflineLibraryUpdates()\n                true\n            } else {\n                false\n            }\n        }\n        initSearchView(toolbar.menu.findItem(R.id.menu_search))\n        updateToolbarMenu(toolbar.menu)\n    }\n\n    @OptIn(ExperimentalBadgeUtils::class)\n    private fun updateToolbarMenu(menu: Menu) {\n        if (menu.isEmpty()) return\n\n        BadgeUtils.detachBadgeDrawable(\n            offlineLibraryUpdateBadge,\n            binding.toolbar,\n            R.id.menu_synchronize\n        )\n\n        val searchMenuItem = menu.findItem(R.id.menu_search)\n        updateSynchronizationMenuItem(!searchMenuItem.isActionViewExpanded)\n    }\n\n    @OptIn(ExperimentalBadgeUtils::class)\n    private fun updateSynchronizationMenuItem(isSearchViewCollapsed: Boolean) {\n        val synchronizeMenuItem = binding.toolbar.menu.findItem(R.id.menu_synchronize) ?: return\n        val showMenuItem = offlineLibraryModificationsAmount > 0 && isSearchViewCollapsed\n        synchronizeMenuItem.isVisible = showMenuItem\n        if (showMenuItem) {\n            offlineLibraryUpdateBadge.apply {\n                isVisible = true\n                number = offlineLibraryModificationsAmount\n            }\n\n            BadgeUtils.attachBadgeDrawable(\n                offlineLibraryUpdateBadge,\n                binding.toolbar,\n                R.id.menu_synchronize\n            )\n        }\n    }\n\n    private fun initSearchView(menuItem: MenuItem) {\n        val searchView = menuItem.actionView as SearchView\n\n        searchView.queryHint = getString(R.string.hint_search)\n\n        val searchQueryText = viewModel.state.value.filter.searchQuery\n        if (searchQueryText.isNotBlank()) {\n            // restore previous search view state\n            menuItem.expandActionView()\n            searchView.post {\n                if (!isAdded) return@post\n                searchView.setQuery(searchQueryText, false)\n            }\n        }\n\n        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {\n            override fun onQueryTextSubmit(query: String?): Boolean {\n                searchDebouncer.debounce(lifecycleScope) {\n                    viewModel.searchLibrary(query ?: \"\")\n                }\n                return false\n            }\n\n            override fun onQueryTextChange(newText: String?): Boolean {\n                searchDebouncer.debounce(lifecycleScope) {\n                    viewModel.searchLibrary(newText ?: \"\")\n                }\n                return false\n            }\n        })\n\n        searchViewBackPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(\n            viewLifecycleOwner,\n            menuItem.isActionViewExpanded\n        ) {\n            menuItem.collapseActionView()\n            isEnabled = false\n        }\n\n        menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {\n            override fun onMenuItemActionExpand(item: MenuItem): Boolean {\n                if (isAdded) {\n                    searchViewBackPressedCallback?.isEnabled = true\n                    updateSynchronizationMenuItem(false)\n                }\n                return true\n            }\n\n            override fun onMenuItemActionCollapse(item: MenuItem): Boolean {\n                viewModel.searchLibrary(\"\")\n                searchViewBackPressedCallback?.isEnabled = false\n                if (isAdded) {\n                    updateSynchronizationMenuItem(true)\n                    if (searchView.query.isNotBlank()) {\n                        binding.rvLibraryEntries.apply {\n                            post {\n                                if (!isAdded) return@post\n                                scrollToPosition(0)\n                            }\n                        }\n                    }\n                }\n                return true\n            }\n        })\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        binding.rvLibraryEntries.smoothScrollToPosition(0)\n        binding.appBarLayout.setExpanded(true)\n    }\n\n    override fun onDestroyView() {\n        viewModel.doRefreshListener = null\n        searchViewBackPressedCallback?.remove()\n        searchViewBackPressedCallback = null\n        super.onDestroyView()\n        _binding = null\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/library/LibraryViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.library\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.PagingData\nimport androidx.paging.TerminalSeparatorType\nimport androidx.paging.cachedIn\nimport androidx.paging.insertSeparators\nimport androidx.paging.map\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.library.LibraryEntryKind\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryFilter\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryUiModel\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryModificationState.NOT_SYNCHRONIZED\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.domain.library.GetLibraryEntriesWithModificationsPagerUseCase\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult\nimport io.github.drumber.kitsune.domain.library.SearchLibraryEntriesWithLocalModificationsPagerUseCase\nimport io.github.drumber.kitsune.domain.library.SynchronizeLocalLibraryModificationsUseCase\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryProgressUseCase\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryRatingUseCase\nimport io.github.drumber.kitsune.domain.user.GetLocalUserIdUseCase\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.library.InternalAction.LibraryUpdateOperationEnd\nimport io.github.drumber.kitsune.ui.library.InternalAction.LibraryUpdateOperationStart\nimport io.github.drumber.kitsune.ui.library.LibraryChangeResult.LibrarySynchronizationResult\nimport io.github.drumber.kitsune.ui.library.LibraryChangeResult.LibraryUpdateResult\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.cancelAndJoin\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.filterIsInstance\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.atomic.AtomicInteger\n\nclass LibraryViewModel(\n    private val userRepository: UserRepository,\n    private val getLocalUserId: GetLocalUserIdUseCase,\n    private val libraryRepository: LibraryRepository,\n    private val getLibraryEntriesWithModifications: GetLibraryEntriesWithModificationsPagerUseCase,\n    private val searchLibraryEntriesWithModification: SearchLibraryEntriesWithLocalModificationsPagerUseCase,\n    private val updateLibraryEntryProgress: UpdateLibraryEntryProgressUseCase,\n    private val updateLibraryEntryRating: UpdateLibraryEntryRatingUseCase,\n    private val synchronizeLocalLibraryModifications: SynchronizeLocalLibraryModificationsUseCase\n) : ViewModel() {\n\n    val state: StateFlow<UiState>\n\n    val pagingDataFlow: Flow<PagingData<LibraryEntryUiModel>>\n\n    val acceptAction: (UiAction) -> Unit\n\n    private val acceptInternalAction: (InternalAction) -> Unit\n\n    /**\n     * The ID of the last updated entry the recycler view should scroll to.\n     */\n    var scrollToUpdatedEntryId: String? = null\n        private set\n\n    var doRefreshListener: (() -> Unit)? = null\n\n    val libraryChangeResultFlow: Flow<LibraryChangeResult>\n\n    val notSynchronizedLibraryEntryModifications =\n        libraryRepository.getLibraryEntryModificationsByStateAsLiveData(NOT_SYNCHRONIZED)\n\n    private val libraryProgressUpdateJobs = ConcurrentHashMap<String, Job>()\n\n    val localUser = userRepository.localUser\n\n    init {\n        val initialFilter = FilterState(\n            KitsunePref.libraryEntryKind,\n            KitsunePref.libraryEntryStatus,\n        )\n        val actionStateFlow = MutableSharedFlow<UiAction>()\n        val searches = actionStateFlow\n            .filterIsInstance<UiAction.Filter>()\n            .distinctUntilChanged()\n            .onStart { emit(UiAction.Filter(initialFilter)) }\n        val queriesScrolled = actionStateFlow\n            .filterIsInstance<UiAction.Scroll?>()\n            .distinctUntilChanged()\n            .onStart { emit(null) }\n\n        val libraryUpdateOperationsCounter = AtomicInteger(0)\n        val internalActionFlow = MutableSharedFlow<InternalAction>()\n        val internalActionState = internalActionFlow\n            .onEach { action ->\n                when (action) {\n                    is LibraryUpdateOperationStart -> libraryUpdateOperationsCounter.incrementAndGet()\n                    is LibraryUpdateOperationEnd -> libraryUpdateOperationsCounter.decrementAndGet()\n                    else -> {}\n                }\n            }\n            .map {\n                InternalState(\n                    libraryOperationsCount = libraryUpdateOperationsCounter.get()\n                )\n            }\n            .distinctUntilChanged()\n            .onStart { emit(InternalState(libraryUpdateOperationsCounter.get())) }\n        libraryChangeResultFlow = internalActionFlow\n            .mapNotNull {\n                when (it) {\n                    is InternalAction.LibraryUpdateResult -> LibraryUpdateResult(it.result)\n                    is InternalAction.LibrarySynchronizationResult -> LibrarySynchronizationResult(\n                        it.results\n                    )\n\n                    else -> null\n                }\n            }\n\n        pagingDataFlow = searches\n            .mapNotNull { createLibraryEntryFilter(it.filter) }\n            .flatMapLatest { filter ->\n                getPagingLibraryEntriesFlow(filter)\n                    .insertSeparators(filter)\n            }\n            .cachedIn(viewModelScope)\n\n        state = combine(\n            searches,\n            queriesScrolled,\n            internalActionState\n        ) { search, scroll, internalState ->\n            UiState(\n                filter = search.filter,\n                hasNotScrolledForCurrentFilter = search.filter != scroll?.currentFilter,\n                isLibraryUpdateOperationInProgress = internalState.libraryOperationsCount != 0\n            )\n        }.stateIn(\n            scope = viewModelScope,\n            started = SharingStarted.Lazily,\n            initialValue = UiState(initialFilter)\n        )\n\n        acceptAction = { inputAction ->\n            val action = if (inputAction is UiAction.Filter) {\n                inputAction.copy(filter = inputAction.filter.copy(createTime = System.currentTimeMillis()))\n            } else {\n                inputAction\n            }\n\n            viewModelScope.launch { actionStateFlow.emit(action) }\n        }\n\n        acceptInternalAction = { action ->\n            viewModelScope.launch { internalActionFlow.emit(action) }\n        }\n    }\n\n    fun hasUser() = userRepository.hasLocalUser()\n\n    private fun getPagingLibraryEntriesFlow(\n        filter: LibraryEntryFilter\n    ): Flow<PagingData<LibraryEntryWithModification>> {\n        return if (filter.isFilteredBySearchQuery()) {\n            // if filter contains a search query, then search directly using the paging source\n            searchLibraryEntriesWithModification(\n                Kitsu.DEFAULT_PAGE_SIZE_LIBRARY,\n                filter.buildFilter(),\n                viewModelScope\n            )\n        } else {\n            // otherwise use the default paging source\n            getLibraryEntriesWithModifications(\n                Kitsu.DEFAULT_PAGE_SIZE_LIBRARY,\n                filter,\n                viewModelScope\n            )\n        }\n    }\n\n    private fun Flow<PagingData<LibraryEntryWithModification>>.insertSeparators(\n        filter: LibraryEntryFilter\n    ): Flow<PagingData<LibraryEntryUiModel>> {\n        return map { pagingData ->\n            pagingData.map { LibraryEntryUiModel.EntryModel(it) }\n        }.map {\n            it.insertSeparators(TerminalSeparatorType.SOURCE_COMPLETE) { before, after ->\n                when {\n                    // do not insert separators if library is currently searched\n                    filter.isFilteredBySearchQuery() -> null\n\n                    after?.entry?.libraryEntry?.status == null -> null\n\n                    before == null || before.entry.libraryEntry.status != after.entry.libraryEntry.status ->\n                        LibraryEntryUiModel.StatusSeparatorModel(\n                            status = after.entry.libraryEntry.status,\n                            isMangaSelected = filter.kind == LibraryEntryKind.Manga\n                        )\n\n                    else -> null\n                }\n            }\n        }\n    }\n\n    private fun createLibraryEntryFilter(filter: FilterState): LibraryEntryFilter? {\n        return getLocalUserId()?.let { userId ->\n            val requestFilter = Filter()\n                .filter(\"user_id\", userId)\n                .sort(\"status\", \"-progressed_at\")\n                .include(\"anime\", \"manga\")\n\n            // if the search query is not blank, add it to the filter and we will search for the given query\n            if (filter.searchQuery.isNotBlank()) {\n                requestFilter.filter(\"title\", filter.searchQuery)\n            }\n\n            LibraryEntryFilter(\n                kind = filter.kind,\n                libraryStatus = filter.libraryStatus,\n                initialFilter = requestFilter\n            )\n        }\n    }\n\n    fun searchLibrary(searchQueryText: String) {\n        val currentFilter = state.value.filter\n        if (currentFilter.searchQuery.trim() != searchQueryText.trim()) {\n            acceptAction(UiAction.Filter(currentFilter.copy(searchQuery = searchQueryText)))\n        }\n    }\n\n    fun invalidatePagingSource() {\n        libraryRepository.invalidatePagingSources()\n    }\n\n    fun setLibraryEntryKind(kind: LibraryEntryKind) {\n        KitsunePref.libraryEntryKind = kind\n        val currentFilter = state.value.filter\n        acceptAction(UiAction.Filter(currentFilter.copy(kind = kind)))\n    }\n\n    fun setLibraryEntryStatus(status: List<LibraryStatus>) {\n        // clear status filter if all filters are selected\n        val statusFilter = if (status.size == 5) emptyList() else status\n        KitsunePref.libraryEntryStatus = statusFilter\n        val currentFilter = state.value.filter\n        acceptAction(UiAction.Filter(currentFilter.copy(libraryStatus = statusFilter)))\n    }\n\n    fun synchronizeOfflineLibraryUpdates() {\n        acceptInternalAction(LibraryUpdateOperationStart)\n        viewModelScope.launch(Dispatchers.IO) {\n            val librarySyncResults = synchronizeLocalLibraryModifications()\n            acceptInternalAction(InternalAction.LibrarySynchronizationResult(librarySyncResults.values.toList()))\n        }.invokeOnCompletion {\n            acceptInternalAction(LibraryUpdateOperationEnd)\n        }\n    }\n\n    fun markEpisodeWatched(libraryEntryWrapper: LibraryEntryWithModification) {\n        val currentProgress = libraryEntryWrapper.progress ?: 0\n        val newProgress = currentProgress + 1\n        updateLibraryProgress(libraryEntryWrapper.libraryEntry, newProgress)\n    }\n\n    fun markEpisodeUnwatched(libraryEntryWrapper: LibraryEntryWithModification) {\n        val currentProgress = libraryEntryWrapper.progress ?: 0\n        if (currentProgress == 0) return\n        val newProgress = currentProgress - 1\n        updateLibraryProgress(libraryEntryWrapper.libraryEntry, newProgress)\n    }\n\n    private fun updateLibraryProgress(libraryEntry: LibraryEntry, newProgress: Int) {\n        val ongoingJob = libraryProgressUpdateJobs[libraryEntry.id]\n        val job = viewModelScope.launch(Dispatchers.IO) {\n            ongoingJob?.cancelAndJoin() // wait until ongoing update call is cancelled\n            performLibraryEntryUpdate {\n                updateLibraryEntryProgress(libraryEntry, newProgress)\n            }\n        }\n\n        libraryProgressUpdateJobs[libraryEntry.id] = job\n        job.invokeOnCompletion {\n            if (libraryProgressUpdateJobs[libraryEntry.id] == job) {\n                libraryProgressUpdateJobs.remove(libraryEntry.id)\n            }\n        }\n    }\n\n    /** Set to the library entry which rating should be updated. */\n    var lastRatedLibraryEntry: LibraryEntry? = null\n\n    fun updateRating(rating: Int?) {\n        val libraryEntry = lastRatedLibraryEntry ?: return\n\n        viewModelScope.launch(Dispatchers.IO) {\n            performLibraryEntryUpdate {\n                updateLibraryEntryRating(libraryEntry, rating)\n            }\n        }\n    }\n\n    private suspend fun performLibraryEntryUpdate(block: suspend () -> LibraryEntryUpdateResult) {\n        acceptInternalAction(LibraryUpdateOperationStart)\n        val updateResult = try {\n            block()\n        } finally {\n            acceptInternalAction(LibraryUpdateOperationEnd)\n        }\n        acceptInternalAction(InternalAction.LibraryUpdateResult(updateResult))\n\n        if (updateResult is LibraryEntryUpdateResult.Success) {\n            scrollToUpdatedEntry(updateResult.updatedLibraryEntry.id)\n        }\n\n        if (updateResult is LibraryEntryUpdateResult.Success && state.value.filter.searchQuery.isNotBlank()) {\n            // trigger new search to show the updated data\n            withContext(Dispatchers.Main) {\n                triggerAdapterUpdate()\n            }\n        }\n    }\n\n    private fun scrollToUpdatedEntry(libraryEntryId: String?) {\n        scrollToUpdatedEntryId = libraryEntryId\n    }\n\n    /**\n     * Signals that the recycler view was scrolled to the updated entry.\n     */\n    fun hasScrolledToUpdatedEntry() {\n        scrollToUpdatedEntryId = null\n    }\n\n    fun triggerAdapterUpdate() {\n        doRefreshListener?.invoke()\n    }\n}\n\nsealed class UiAction {\n    data class Filter(val filter: FilterState) : UiAction()\n    data class Scroll(val currentFilter: FilterState) : UiAction()\n}\n\ndata class UiState(\n    val filter: FilterState,\n    val hasNotScrolledForCurrentFilter: Boolean = true,\n    val isLibraryUpdateOperationInProgress: Boolean = false\n)\n\ndata class FilterState(\n    val kind: LibraryEntryKind = LibraryEntryKind.All,\n    val libraryStatus: List<LibraryStatus> = emptyList(),\n    val searchQuery: String = \"\",\n    val createTime: Long = System.currentTimeMillis(),\n)\n\nsealed class LibraryChangeResult {\n    data class LibraryUpdateResult(val result: LibraryEntryUpdateResult) : LibraryChangeResult()\n    data class LibrarySynchronizationResult(val results: List<LibraryEntryUpdateResult>) :\n        LibraryChangeResult()\n}\n\nprivate sealed class InternalAction {\n    data object LibraryUpdateOperationStart : InternalAction()\n    data object LibraryUpdateOperationEnd : InternalAction()\n    data class LibraryUpdateResult(val result: LibraryEntryUpdateResult) : InternalAction()\n    data class LibrarySynchronizationResult(val results: List<LibraryEntryUpdateResult>) :\n        InternalAction()\n}\n\nprivate data class InternalState(\n    val libraryOperationsCount: Int\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/library/RatingBottomSheet.kt",
    "content": "package io.github.drumber.kitsune.ui.library\n\nimport android.annotation.SuppressLint\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.os.bundleOf\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.setFragmentResult\nimport androidx.navigation.fragment.navArgs\nimport com.google.android.material.bottomsheet.BottomSheetDialogFragment\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.SheetLibraryRatingBinding\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.convertFrom\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.convertToRatingTwenty\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.fromRatingTwentyTo\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.stepSize\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.toRatingTwentyFrom\n\nclass RatingBottomSheet : BottomSheetDialogFragment() {\n\n    private var _binding: SheetLibraryRatingBinding? = null\n    private val binding get() = _binding!!\n\n    private val args: RatingBottomSheetArgs by navArgs()\n\n    private val ratingSystem\n        get() = args.ratingSystem\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = SheetLibraryRatingBinding.inflate(inflater, container, false)\n\n        val ratingTwenty = args.ratingTwenty.takeIf { it != -1 }\n        val hasNoRating = ratingTwenty == null || ratingTwenty == 0\n\n        binding.apply {\n            title = args.title\n\n            ratingBar.apply {\n                setOnRatingChangeListener { ratingBar, rating ->\n                    val isInRange = ratingSystem.convertToRatingTwenty(rating) in 1..20\n                    if (!isInRange) {\n                        val coercedRatingTwenty = ratingSystem.convertToRatingTwenty(rating)\n                            .coerceIn(1..20)\n                        ratingBar.post {\n                            if (!isAdded) return@post\n                            ratingBar.rating = ratingSystem.convertFrom(coercedRatingTwenty)\n                        }\n                    }\n                    btnRate.isEnabled = isInRange\n                    updateRatingTextView()\n                }\n                numStars = ratingSystem.convertFrom(20).toInt()\n                stepSize = ratingSystem.stepSize()\n                ratingTwenty?.fromRatingTwentyTo(ratingSystem)?.let {\n                    rating = it\n                }\n            }\n\n            btnCancel.setOnClickListener { dismiss() }\n            btnRate.setOnClickListener { onRateClicked() }\n            btnRate.isEnabled = !hasNoRating\n            btnRate.setText(if (hasNoRating) R.string.action_rate else R.string.action_update_rating)\n            btnRemoveRating.setOnClickListener { onRemoveRatingClicked() }\n            btnRemoveRating.isVisible = !hasNoRating\n        }\n\n        updateRatingTextView()\n\n        return binding.root\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    private fun updateRatingTextView() {\n        val rating = binding.ratingBar.rating\n        binding.tvRating.text = if (rating == 0.0f) {\n            getString(R.string.library_not_rated)\n        } else {\n            \"$rating / ${RatingSystemUtil.formatRating(20, ratingSystem)}\"\n        }\n    }\n\n    private fun onRateClicked() {\n        val ratingTwenty = binding.ratingBar.rating.toRatingTwentyFrom(ratingSystem)\n        setFragmentResult(args.ratingResultKey, bundleOf(BUNDLE_RATING to ratingTwenty))\n        dismiss()\n    }\n\n    private fun onRemoveRatingClicked() {\n        setFragmentResult(args.removeResultKey, bundleOf(BUNDLE_RATING to null))\n        dismiss()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        _binding = null\n    }\n\n    companion object {\n        const val BUNDLE_RATING = \"rating_bundle_key\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/library/editentry/LibraryEditEntryFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.library.editentry\n\nimport android.content.res.ColorStateList\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.WindowManager\nimport android.widget.ArrayAdapter\nimport android.widget.AutoCompleteTextView\nimport androidx.appcompat.widget.TooltipCompat\nimport androidx.core.content.ContextCompat\nimport androidx.core.os.bundleOf\nimport androidx.core.text.htmlEncode\nimport androidx.core.text.parseAsHtml\nimport androidx.core.view.isVisible\nimport androidx.core.widget.doAfterTextChanged\nimport androidx.fragment.app.setFragmentResult\nimport androidx.fragment.app.setFragmentResultListener\nimport androidx.navigation.fragment.findNavController\nimport androidx.navigation.fragment.navArgs\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.load.resource.bitmap.RoundedCorners\nimport com.google.android.material.datepicker.CalendarConstraints\nimport com.google.android.material.datepicker.DateValidatorPointBackward\nimport com.google.android.material.datepicker.MaterialDatePicker\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.snackbar.Snackbar\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.addTransform\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.library.getStringResId\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.databinding.FragmentEditLibraryEntryBinding\nimport io.github.drumber.kitsune.ui.base.BaseDialogFragment\nimport io.github.drumber.kitsune.ui.component.CustomNumberSpinner\nimport io.github.drumber.kitsune.ui.library.RatingBottomSheet\nimport io.github.drumber.kitsune.ui.library.editentry.LibraryEditEntryViewModel.LoadState\nimport io.github.drumber.kitsune.util.DATE_FORMAT_ISO\nimport io.github.drumber.kitsune.util.extensions.getResourceId\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.formatDate\nimport io.github.drumber.kitsune.util.formatUtcDate\nimport io.github.drumber.kitsune.util.getLocalCalendar\nimport io.github.drumber.kitsune.util.parseUtcDate\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.formatRatingTwenty\nimport io.github.drumber.kitsune.util.stripTimeUtcMillis\nimport io.github.drumber.kitsune.util.toDate\nimport io.github.drumber.kitsune.util.ui.DateValidatorPointBetween\nimport io.github.drumber.kitsune.util.ui.initImePaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initMarginWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass LibraryEditEntryFragment : BaseDialogFragment(R.layout.fragment_edit_library_entry) {\n\n    private val args: LibraryEditEntryFragmentArgs by navArgs()\n\n    private var _binding: FragmentEditLibraryEntryBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: LibraryEditEntryViewModel by viewModel()\n\n    private var listenersInitialized = false\n\n    companion object {\n        const val RESULT_KEY_RATING = \"library_edit_rating_result_key\"\n        const val RESULT_KEY_REMOVE_RATING = \"library_edit_remove_rating_result_key\"\n    }\n\n    private val libraryStatusMenuItems = listOf(\n        LibraryStatus.Current,\n        LibraryStatus.Planned,\n        LibraryStatus.Completed,\n        LibraryStatus.OnHold,\n        LibraryStatus.Dropped\n    )\n\n    override fun onStart() {\n        requireDialog().window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)\n        super.onStart()\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentEditLibraryEntryBinding.inflate(inflater, container, false)\n        viewModel.initLibraryEntry(args.libraryEntryId)\n\n        binding.toolbar.initWindowInsetsListener(consume = false)\n        binding.nestedScrollView.initMarginWindowInsetsListener(\n            left = true,\n            right = true,\n            consume = false\n        )\n        binding.layoutBottomBar.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            bottom = true,\n            consume = false\n        )\n\n        binding.root.initImePaddingWindowInsetsListener()\n\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        binding.apply {\n            toolbar.setNavigationOnClickListener {\n                dismiss()\n            }\n\n            TooltipCompat.setTooltipText(\n                btnRemoveEntry,\n                getString(R.string.library_action_remove)\n            )\n\n            btnSaveChanges.setOnClickListener {\n                viewModel.saveChanges()\n            }\n            btnRemoveEntry.setOnClickListener {\n                val libraryEntry = viewModel.libraryEntry.value\n\n                val dialogMsg = getString(\n                    R.string.dialog_remove_from_library_msg,\n                    libraryEntry?.media?.title?.htmlEncode() ?: getString(R.string.no_information)\n                ).parseAsHtml()\n\n                MaterialAlertDialogBuilder(requireContext())\n                    .setTitle(R.string.dialog_remove_from_library_title)\n                    .setMessage(dialogMsg)\n                    .setNegativeButton(android.R.string.cancel) { dialog, _ ->\n                        dialog.dismiss()\n                    }\n                    .setPositiveButton(R.string.action_remove) { dialog, _ ->\n                        dialog.dismiss()\n                        viewModel.removeLibraryEntry()\n                    }\n                    .show()\n            }\n        }\n\n        viewModel.loadState.observe(viewLifecycleOwner) { state ->\n            if (state == LoadState.CloseDialog) {\n                args.entryUpdatedResultKey?.let {\n                    setFragmentResult(it, bundleOf())\n                }\n                dismiss()\n            } else {\n                binding.layoutLoading.isVisible = state == LoadState.Loading\n                if (state == LoadState.Error) {\n                    Snackbar.make(\n                        binding.root,\n                        R.string.error_library_update_failed,\n                        Snackbar.LENGTH_LONG\n                    )\n                        .setAnchorView(binding.cardBottomBar)\n                        .show()\n                }\n            }\n        }\n\n        viewModel.libraryEntry.observe(viewLifecycleOwner) { libraryEntry ->\n            val media = libraryEntry.media\n\n            binding.tvTitle.text = media?.title\n\n            binding.tvMediaInfo.text = media?.let { \"${it.publishingYearText(binding.root.context)} • ${it.subtypeFormatted}\" }\n\n            Glide.with(this)\n                .load(media?.posterImageUrl)\n                .addTransform(RoundedCorners(8))\n                .placeholder(R.drawable.ic_insert_photo_48)\n                .into(binding.ivThumbnail)\n        }\n\n        viewModel.libraryEntryWithModification.observe(viewLifecycleOwner) { libraryEntry ->\n            val media = libraryEntry.media\n\n            binding.apply {\n                setLibraryStatusMenu(media, libraryEntry.status)\n\n                media?.episodeOrChapterCount?.let {\n                    spinnerProgress.setMaxValue(it)\n                } ?: spinnerProgress.setSuffixMode(CustomNumberSpinner.SuffixMode.Disabled)\n                spinnerProgress.setValue(libraryEntry.progress ?: 0)\n\n                layoutVolumes.isVisible = media is Manga\n                spinnerVolumes.setMaxValue((media as? Manga)?.volumeCount ?: 0)\n                spinnerVolumes.setValue(libraryEntry.volumesOwned ?: 0)\n\n                val ratingTwenty = libraryEntry.ratingTwenty\n                val hasRated = ratingTwenty != null && ratingTwenty != -1\n                fieldRating.editText?.apply {\n                    val ratingText = if (hasRated) {\n                        \"${ratingTwenty!!.formatRatingTwenty()} / ${20.formatRatingTwenty()}\"\n                    } else {\n                        getString(R.string.library_not_rated)\n                    }\n                    setText(ratingText)\n                }\n                fieldRating.setEndIconDrawable(\n                    if (hasRated)\n                        R.drawable.ic_star_24\n                    else\n                        R.drawable.ic_star_outline_24\n                )\n                fieldRating.setEndIconTintList(ColorStateList.valueOf(getControlColor(hasRated)))\n\n                tvReconsumeLabel.text = getString(\n                    if (media is Anime)\n                        R.string.library_edit_rewatch_count\n                    else\n                        R.string.library_edit_reread_count\n                )\n                spinnerReconsume.setValue(libraryEntry.reconsumeCount ?: 0)\n                spinnerReconsume.setActionTooltip(\n                    getString(\n                        if (media is Anime)\n                            R.string.library_edit_start_rewatch\n                        else\n                            R.string.library_edit_start_reread\n                    )\n                )\n\n                val privacyAdapter = ArrayAdapter(\n                    requireContext(),\n                    R.layout.item_dropdown,\n                    listOf(\n                        getString(R.string.library_edit_privacy_public),\n                        getString(R.string.library_edit_privacy_private)\n                    )\n                )\n                (menuPrivacy.editText as? AutoCompleteTextView)?.apply {\n                    setAdapter(privacyAdapter)\n                    val currValue = if (libraryEntry.isPrivate == true)\n                        getString(R.string.library_edit_privacy_private)\n                    else\n                        getString(R.string.library_edit_privacy_public)\n                    setText(currValue, false)\n                }\n\n                val startedText = libraryEntry.startedAt?.parseUtcDate()?.formatDate()\n                    ?: getString(R.string.library_edit_no_date_set)\n                fieldStarted.editText?.setText(startedText)\n                btnClearStarted.isVisible = !libraryEntry.startedAt.isNullOrEmpty()\n\n                val finishedText = libraryEntry.finishedAt?.parseUtcDate()?.formatDate()\n                    ?: getString(R.string.library_edit_no_date_set)\n                fieldFinished.editText?.setText(finishedText)\n                btnClearFinished.isVisible = !libraryEntry.finishedAt.isNullOrEmpty()\n\n                fieldNotes.editText?.apply {\n                    if (text.toString() != (libraryEntry.notes ?: \"\")) {\n                        setText(libraryEntry.notes)\n                    }\n                }\n            }\n\n            if (!listenersInitialized) {\n                initListeners()\n            }\n        }\n\n        viewModel.hasChanges.observe(viewLifecycleOwner) { hasChanges ->\n            binding.btnSaveChanges.isEnabled = hasChanges\n        }\n\n        setFragmentResultListener(RESULT_KEY_RATING) { _, bundle ->\n            val rating = bundle.getInt(RatingBottomSheet.BUNDLE_RATING, -1)\n            if (rating != -1) {\n                viewModel.updateLibraryEntry { it.copy(ratingTwenty = rating) }\n            }\n        }\n\n        setFragmentResultListener(RESULT_KEY_REMOVE_RATING) { _, _ ->\n            val oldRating = viewModel.uneditedLibraryEntryWrapper?.ratingTwenty\n            val rating = if (oldRating == null) null else -1\n            viewModel.updateLibraryEntry { it.copy(ratingTwenty = rating) }\n        }\n    }\n\n    private fun initListeners() {\n        listenersInitialized = true\n        binding.apply {\n            (menuLibraryStatus.editText as? AutoCompleteTextView)?.setOnItemClickListener { _, _, position, _ ->\n                val status = libraryStatusMenuItems[position]\n                viewModel.updateLibraryEntry { it.copy(status = status) }\n            }\n\n            spinnerProgress.setValueChangedListener { value ->\n                val wrapper = viewModel.libraryEntryWithModification.value\n\n                if (value == wrapper?.media?.episodeOrChapterCount) {\n                    viewModel.updateLibraryEntry {\n                        it.copy(\n                            progress = value,\n                            status = LibraryStatus.Completed,\n                            finishedAt = wrapper.finishedAt ?: getLocalCalendar().formatUtcDate()\n                        )\n                    }\n                } else {\n                    viewModel.updateLibraryEntry { it.copy(progress = value) }\n                }\n            }\n\n            spinnerVolumes.setValueChangedListener { value ->\n                viewModel.updateLibraryEntry { it.copy(volumesOwned = value) }\n            }\n\n            fieldRating.editText?.setOnClickListener {\n                showRatingBottomSheet()\n            }\n\n            spinnerReconsume.setValueChangedListener { value ->\n                viewModel.updateLibraryEntry { it.copy(reconsumeCount = value) }\n            }\n\n            // start reconsume action\n            spinnerReconsume.setActionClickListener {\n                val wrapper = viewModel.libraryEntryWithModification.value\n                viewModel.updateLibraryEntry {\n                    it.copy(\n                        progress = 0,\n                        reconsumeCount = wrapper?.reconsumeCount?.plus(1) ?: 1,\n                        status = LibraryStatus.Current\n                    )\n                }\n            }\n\n            (menuPrivacy.editText as? AutoCompleteTextView)?.setOnItemClickListener { _, _, position, _ ->\n                val isPrivate = position == 1\n                viewModel.updateLibraryEntry { it.copy(privateEntry = isPrivate) }\n            }\n\n            fieldStarted.editText?.setOnClickListener {\n                val wrapper = viewModel.libraryEntryWithModification.value\n                val selection = wrapper?.startedAt?.parseUtcDate()?.time\n                    ?.stripTimeUtcMillis() ?: MaterialDatePicker.todayInUtcMilliseconds()\n\n                val validator = wrapper?.finishedAt?.parseUtcDate()?.time\n                    ?.stripTimeUtcMillis()?.let {\n                        DateValidatorPointBackward.before(it)\n                    } ?: DateValidatorPointBackward.now()\n\n                openDatePicker(\n                    getString(R.string.library_edit_started),\n                    selection,\n                    validator\n                ) { dateMillis ->\n                    val dateString = dateMillis.toDate().formatDate(DATE_FORMAT_ISO)\n                    viewModel.updateLibraryEntry { it.copy(startedAt = dateString) }\n                }\n            }\n\n            btnClearStarted.setOnClickListener {\n                viewModel.updateLibraryEntry { it.copy(startedAt = \"\") }\n            }\n\n            fieldFinished.editText?.setOnClickListener {\n                val wrapper = viewModel.libraryEntryWithModification.value\n                val selection = wrapper?.finishedAt?.parseUtcDate()?.time\n                    ?.stripTimeUtcMillis() ?: MaterialDatePicker.todayInUtcMilliseconds()\n\n                val validator = wrapper?.startedAt?.parseUtcDate()?.time\n                    ?.stripTimeUtcMillis()?.let {\n                        DateValidatorPointBetween.nowAndFrom(it)\n                    } ?: DateValidatorPointBackward.now()\n\n                openDatePicker(\n                    getString(R.string.library_edit_finished),\n                    selection,\n                    validator\n                ) { dateMillis ->\n                    val dateString = dateMillis.toDate().formatDate(DATE_FORMAT_ISO)\n                    viewModel.updateLibraryEntry { it.copy(finishedAt = dateString) }\n                }\n            }\n\n            btnClearFinished.setOnClickListener {\n                viewModel.updateLibraryEntry { it.copy(finishedAt = \"\") }\n            }\n\n            fieldNotes.editText?.doAfterTextChanged { text ->\n                val oldNotes = viewModel.uneditedLibraryEntryWrapper?.notes\n                val note = if (oldNotes == null && text?.toString().isNullOrBlank()) {\n                    // if old note is null and we haven't edited the note, then set it to null\n                    null\n                } else {\n                    text?.toString()\n                }\n                viewModel.updateLibraryEntry { it.copy(notes = note) }\n            }\n        }\n    }\n\n    private fun setLibraryStatusMenu(media: Media?, currValue: LibraryStatus?) {\n        val isAnime = media is Anime\n        val statusItems = libraryStatusMenuItems.map { getString(it.getStringResId(isAnime)) }\n\n        val adapter = ArrayAdapter(\n            requireContext(),\n            R.layout.item_dropdown,\n            statusItems\n        )\n\n        (binding.menuLibraryStatus.editText as? AutoCompleteTextView)?.apply {\n            setAdapter(adapter)\n            currValue?.let { setText(getString(it.getStringResId(isAnime)), false) }\n        }\n    }\n\n    private fun showRatingBottomSheet() {\n        val libraryEntryWrapper = viewModel.libraryEntryWithModification.value ?: return\n        val libraryEntry = viewModel.libraryEntryWithModification.value?.libraryEntry ?: return\n        val media = libraryEntry.media ?: return\n\n        val action =\n            LibraryEditEntryFragmentDirections.actionLibraryEditEntryFragmentToRatingBottomSheet(\n                title = media.title ?: \"\",\n                ratingTwenty = libraryEntryWrapper.ratingTwenty ?: -1,\n                ratingResultKey = RESULT_KEY_RATING,\n                removeResultKey = RESULT_KEY_REMOVE_RATING,\n                ratingSystem = RatingSystemUtil.getRatingSystem()\n            )\n        findNavController().navigateSafe(R.id.libraryEditEntryFragment, action)\n    }\n\n    private fun openDatePicker(\n        title: String,\n        selection: Long,\n        validator: CalendarConstraints.DateValidator,\n        action: (Long) -> Unit\n    ) {\n        val constraints = CalendarConstraints.Builder()\n            .setValidator(validator)\n            .setEnd(MaterialDatePicker.todayInUtcMilliseconds())\n            .build()\n\n        val datePicker = MaterialDatePicker.Builder.datePicker()\n            .setTitleText(title)\n            .setSelection(selection)\n            .setCalendarConstraints(constraints)\n            .build()\n\n        datePicker.addOnPositiveButtonClickListener(action)\n        datePicker.show(parentFragmentManager, \"DATE_PICKER_$title\")\n    }\n\n    private fun getControlColor(accent: Boolean): Int {\n        return ContextCompat.getColor(\n            requireContext(), requireActivity().theme.getResourceId(\n                if (accent) R.attr.colorPrimary else R.attr.colorControlNormal\n            )\n        )\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/library/editentry/LibraryEditEntryViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.library.editentry\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.map\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateFailureReason.NotFound\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult.Failure\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult.Success\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryUseCase\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass LibraryEditEntryViewModel(\n    private val libraryRepository: LibraryRepository,\n    private val updateLibraryEntryUseCase: UpdateLibraryEntryUseCase\n) : ViewModel() {\n\n    var uneditedLibraryEntryWrapper: LibraryEntryWithModification? = null\n        private set\n\n    private val _libraryEntryWithModification = MutableLiveData<LibraryEntryWithModification>()\n    val libraryEntryWithModification\n        get() = _libraryEntryWithModification as LiveData<LibraryEntryWithModification>\n\n    private val _libraryEntry = MutableLiveData<LibraryEntry>()\n    val libraryEntry\n        get() = _libraryEntry as LiveData<LibraryEntry>\n\n    private val _loadState = MutableLiveData(LoadState.NotLoading)\n    val loadState\n        get() = _loadState as LiveData<LoadState>\n\n    val hasChanges: LiveData<Boolean> = libraryEntryWithModification.map {\n        val uneditedWrapper = uneditedLibraryEntryWrapper ?: return@map false\n        val entry = uneditedWrapper.libraryEntry.copy()\n        // apply old modifications\n        val oldModifiedEntry = uneditedWrapper.modification\n            ?.applyToLibraryEntry(entry)\n            ?: entry\n        // apply new modifications\n        val newModifiedEntry = it.modification\n            ?.applyToLibraryEntry(entry)\n            ?: entry\n        oldModifiedEntry != newModifiedEntry\n    }\n\n    fun initLibraryEntry(libraryEntryId: String) {\n        if (_libraryEntryWithModification.value != null) return\n\n        _loadState.value = LoadState.Loading\n        viewModelScope.launch(Dispatchers.IO) {\n            val libraryEntry = getLibraryEntry(libraryEntryId) ?: run {\n                _loadState.postValue(LoadState.CloseDialog)\n                return@launch\n            }\n            _libraryEntry.postValue(libraryEntry)\n\n            val libraryModification = libraryRepository.getLibraryEntryModification(libraryEntryId)\n                    ?: LibraryEntryModification.withIdAndNulls(libraryEntryId)\n\n            val libraryEntryWrapper = LibraryEntryWithModification(\n                libraryEntry,\n                libraryModification\n            )\n            uneditedLibraryEntryWrapper = libraryEntryWrapper.copy()\n            _libraryEntryWithModification.postValue(libraryEntryWrapper)\n        }.invokeOnCompletion {\n            _loadState.postValue(LoadState.NotLoading)\n        }\n    }\n\n    private suspend fun getLibraryEntry(libraryEntryId: String): LibraryEntry? {\n        return try {\n            libraryRepository.getLibraryEntryFromDatabase(libraryEntryId)\n                ?: libraryRepository.fetchLibraryEntry(\n                    libraryEntryId,\n                    Filter().include(\"anime\", \"manga\")\n                )\n        } catch (e: Exception) {\n            logE(\"Failed to obtain library entry.\", e)\n            return null\n        }\n    }\n\n    fun setLibraryModification(libraryModification: LibraryEntryModification) {\n        libraryEntryWithModification.value\n            ?.copy(modification = libraryModification)\n            ?.let { updatedWrapper ->\n                _libraryEntryWithModification.value = updatedWrapper\n            }\n    }\n\n    fun updateLibraryEntry(block: (LibraryEntryModification) -> LibraryEntryModification) {\n        val updatedLibraryModification = libraryEntryWithModification.value?.modification?.let(block)\n        if (updatedLibraryModification != null) {\n            setLibraryModification(updatedLibraryModification)\n        }\n    }\n\n    fun saveChanges() {\n        val libraryModification = libraryEntryWithModification.value?.modification ?: return\n\n        _loadState.value = LoadState.Loading\n        viewModelScope.launch(Dispatchers.IO) {\n            val result = updateLibraryEntryUseCase.invoke(libraryModification)\n            when {\n                result is Success -> _loadState.postValue(LoadState.CloseDialog)\n                result is Failure && result.reason is NotFound -> _loadState.postValue(LoadState.CloseDialog)\n                result is Failure -> _loadState.postValue(LoadState.Error)\n            }\n        }\n    }\n\n    fun removeLibraryEntry() {\n        val libraryEntryId = libraryEntry.value?.id ?: return\n        _loadState.value = LoadState.Loading\n        viewModelScope.launch(Dispatchers.IO) {\n            try {\n                libraryRepository.removeLibraryEntry(libraryEntryId)\n                _loadState.postValue(LoadState.CloseDialog)\n            } catch (e: Exception) {\n                logE(\"Failed to remove library entry.\", e)\n                _loadState.postValue(LoadState.Error)\n            }\n        }\n    }\n\n    enum class LoadState {\n        NotLoading,\n        Loading,\n        Error,\n        CloseDialog\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/main/HomeExploreFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.main\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.annotation.StringRes\nimport androidx.core.view.doOnPreDraw\nimport androidx.core.view.isVisible\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.lifecycleScope\nimport com.bumptech.glide.Glide\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaDto\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.presentation.model.media.MediaSelector\nimport io.github.drumber.kitsune.data.presentation.model.media.RequestType\nimport io.github.drumber.kitsune.databinding.FragmentHomeExploreBinding\nimport io.github.drumber.kitsune.databinding.SectionMainExploreBinding\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.adapter.OnItemClickListener\nimport io.github.drumber.kitsune.ui.base.BaseFragment\nimport io.github.drumber.kitsune.ui.component.ExploreSection\nimport io.github.drumber.kitsune.ui.main.MainFragmentViewModel.NavigationAction\nimport io.github.drumber.kitsune.util.network.ResponseData\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.navigation.koinNavGraphViewModel\n\nclass HomeExploreFragment : BaseFragment(R.layout.fragment_home_explore),\n    OnItemClickListener<Media> {\n\n    private var _binding: FragmentHomeExploreBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: MainFragmentViewModel by koinNavGraphViewModel(R.id.main_nav_graph)\n\n    companion object {\n        const val BUNDLE_MEDIA_TYPE = \"bundle_media_type\"\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentHomeExploreBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        postponeEnterTransition()\n        view.doOnPreDraw { startPostponedEnterTransition() }\n\n        val mediaType = arguments?.takeIf { it.containsKey(BUNDLE_MEDIA_TYPE) }?.let {\n            it.getSerializable(BUNDLE_MEDIA_TYPE) as? MediaType\n        }\n\n        if (mediaType == MediaType.Anime) {\n            initAnimeExploreSections()\n        } else if (mediaType == MediaType.Manga) {\n            initMangaExploreSections()\n        }\n    }\n\n    private fun initAnimeExploreSections() {\n        // trending\n        buildExploreSectionView(\n            MediaType.Anime,\n            R.string.section_trending,\n            Filter().limit(30),\n            RequestType.TRENDING,\n            binding.sectionTrending,\n            viewModel.getAnimeExploreLiveData(MainFragmentViewModel.TRENDING) as LiveData<ResponseData<List<Media>>>\n        )\n\n        // top airing\n        buildExploreSectionView(\n            MediaType.Anime,\n            R.string.section_top_airing_anime,\n            MainFragmentViewModel.FILTER_TOP_AIRING_ANIME,\n            RequestType.ALL,\n            binding.sectionTopAiring,\n            viewModel.getAnimeExploreLiveData(MainFragmentViewModel.TOP_AIRING) as LiveData<ResponseData<List<Media>>>\n        )\n\n        // top upcoming\n        buildExploreSectionView(\n            MediaType.Anime,\n            R.string.section_top_upcoming_anime,\n            MainFragmentViewModel.FILTER_TOP_UPCOMING_ANIME,\n            RequestType.ALL,\n            binding.sectionTopUpcoming,\n            viewModel.getAnimeExploreLiveData(MainFragmentViewModel.TOP_UPCOMING) as LiveData<ResponseData<List<Media>>>\n        )\n\n        // highest rated\n        buildExploreSectionView(\n            MediaType.Anime,\n            R.string.section_highest_rated_anime,\n            MainFragmentViewModel.FILTER_HIGHEST_RATED_ANIME,\n            RequestType.ALL,\n            binding.sectionHighestRated,\n            viewModel.getAnimeExploreLiveData(MainFragmentViewModel.HIGHEST_RATED) as LiveData<ResponseData<List<Media>>>\n        )\n\n        // most popular\n        buildExploreSectionView(\n            MediaType.Anime,\n            R.string.section_most_popular_anime,\n            MainFragmentViewModel.FILTER_MOST_POPULAR_ANIME,\n            RequestType.ALL,\n            binding.sectionMostPopular,\n            viewModel.getAnimeExploreLiveData(MainFragmentViewModel.MOST_POPULAR) as LiveData<ResponseData<List<Media>>>\n        )\n    }\n\n    private fun initMangaExploreSections() {\n        // trending\n        buildExploreSectionView(\n            MediaType.Manga,\n            R.string.section_trending,\n            Filter().limit(30),\n            RequestType.TRENDING,\n            binding.sectionTrending,\n            viewModel.getMangaExploreLiveData(MainFragmentViewModel.TRENDING) as LiveData<ResponseData<List<Media>>>\n        )\n\n        // top airing\n        buildExploreSectionView(\n            MediaType.Manga,\n            R.string.section_top_airing_manga,\n            MainFragmentViewModel.FILTER_TOP_AIRING_MANGA,\n            RequestType.ALL,\n            binding.sectionTopAiring,\n            viewModel.getMangaExploreLiveData(MainFragmentViewModel.TOP_AIRING) as LiveData<ResponseData<List<Media>>>\n        )\n\n        // top upcoming\n        buildExploreSectionView(\n            MediaType.Manga,\n            R.string.section_top_upcoming_manga,\n            MainFragmentViewModel.FILTER_TOP_UPCOMING_MANGA,\n            RequestType.ALL,\n            binding.sectionTopUpcoming,\n            viewModel.getMangaExploreLiveData(MainFragmentViewModel.TOP_UPCOMING) as LiveData<ResponseData<List<Media>>>\n        )\n\n        // highest rated\n        buildExploreSectionView(\n            MediaType.Manga,\n            R.string.section_highest_rated_manga,\n            MainFragmentViewModel.FILTER_HIGHEST_RATED_MANGA,\n            RequestType.ALL,\n            binding.sectionHighestRated,\n            viewModel.getMangaExploreLiveData(MainFragmentViewModel.HIGHEST_RATED) as LiveData<ResponseData<List<Media>>>\n        )\n\n        // most popular\n        buildExploreSectionView(\n            MediaType.Manga,\n            R.string.section_most_popular_manga,\n            MainFragmentViewModel.FILTER_MOST_POPULAR_MANGA,\n            RequestType.ALL,\n            binding.sectionMostPopular,\n            viewModel.getMangaExploreLiveData(MainFragmentViewModel.MOST_POPULAR) as LiveData<ResponseData<List<Media>>>\n        )\n    }\n\n    private fun buildExploreSectionView(\n        mediaType: MediaType,\n        @StringRes titleRes: Int,\n        filter: Filter,\n        requestType: RequestType,\n        sectionBinding: SectionMainExploreBinding,\n        liveData: LiveData<ResponseData<List<Media>>>\n    ): ExploreSection {\n        sectionBinding.apply {\n            rvMedia.isVisible = false\n            rvMedia.uniqueId = sectionBinding.root.id xor mediaType.ordinal\n            layoutLoading.apply {\n                root.layoutParams.height =\n                    resources.getDimensionPixelSize(KitsunePref.mediaItemSize.heightRes)\n                tvError.isVisible = false\n                btnRetry.isVisible = false\n                root.isVisible = true\n            }\n        }\n\n        val mediaSelector = MediaSelector(mediaType, filter.options, requestType)\n        val section = createExploreSection(titleRes, mediaSelector, sectionBinding.root)\n\n        liveData.observe(viewLifecycleOwner) { response ->\n            when (response) {\n                is ResponseData.Success if response.data.isNotEmpty() -> {\n                    section.setData(response.data)\n                    sectionBinding.apply {\n                        layoutLoading.root.isVisible = false\n                        rvMedia.isVisible = true\n                    }\n                }\n\n                is ResponseData.Success if response.data.isEmpty() -> {\n                    sectionBinding.apply {\n                        rvMedia.isVisible = false\n                        layoutLoading.apply {\n                            root.isVisible = true\n                            tvError.isVisible = false\n                            tvNoData.isVisible = true\n                            progressBar.isVisible = false\n                        }\n                    }\n                }\n\n                else -> {\n                    sectionBinding.apply {\n                        rvMedia.isVisible = false\n                        layoutLoading.apply {\n                            root.isVisible = true\n                            tvError.isVisible = true\n                            tvNoData.isVisible = false\n                            progressBar.isVisible = false\n                        }\n                    }\n                }\n            }\n\n        }\n        return section\n    }\n\n    private fun createExploreSection(\n        @StringRes titleRes: Int,\n        mediaSelector: MediaSelector,\n        view: View\n    ): ExploreSection {\n        val title = getString(titleRes)\n        val glide = Glide.with(this)\n\n        val section = ExploreSection(glide, title, null, this) {\n            viewLifecycleOwner.lifecycleScope.launch {\n                viewModel.navigate(NavigationAction.OpenMediaList(mediaSelector, title))\n            }\n        }\n        section.bindView(view)\n        return section\n    }\n\n    override fun onItemClick(view: View, item: Media) {\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.navigate(NavigationAction.OpenMediaDetails(item.toMediaDto(), view))\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/main/HomeExploreViewPagerAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.main\n\nimport androidx.core.os.bundleOf\nimport androidx.fragment.app.Fragment\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport io.github.drumber.kitsune.data.common.media.MediaType\n\nclass HomeExploreViewPagerAdapter(fragment: Fragment) :\n    FragmentStateAdapter(fragment.childFragmentManager, fragment.viewLifecycleOwner.lifecycle) {\n\n    override fun getItemCount() = 2\n\n    override fun createFragment(position: Int): Fragment {\n        return when (position) {\n            0 -> HomeExploreFragment().apply {\n                arguments = bundleOf(HomeExploreFragment.BUNDLE_MEDIA_TYPE to MediaType.Anime)\n            }\n            1 -> HomeExploreFragment().apply {\n                arguments = bundleOf(HomeExploreFragment.BUNDLE_MEDIA_TYPE to MediaType.Manga)\n            }\n            else -> throw IllegalStateException(\"Invalid position '$position'. There are ony 2 fragments!\")\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/main/MainActivity.kt",
    "content": "package io.github.drumber.kitsune.ui.main\n\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.graphics.drawable.Drawable\nimport android.os.Bundle\nimport android.view.View\nimport androidx.activity.result.contract.ActivityResultContracts.RequestPermission\nimport androidx.core.graphics.Insets\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.isVisible\nimport androidx.core.view.iterator\nimport androidx.core.view.updatePadding\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentManager\nimport androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport androidx.navigation.NavController\nimport androidx.navigation.NavDestination\nimport androidx.navigation.NavOptions\nimport androidx.navigation.fragment.NavHostFragment\nimport androidx.navigation.ui.NavigationUI\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.request.target.CustomTarget\nimport com.bumptech.glide.request.transition.Transition\nimport com.google.android.material.bottomnavigation.BottomNavigationView\nimport com.google.android.material.navigation.NavigationBarView\nimport com.google.android.material.navigationrail.NavigationRailView\nimport com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback\nimport io.github.drumber.kitsune.BuildConfig\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.IntentAction.OPEN_LIBRARY\nimport io.github.drumber.kitsune.constants.IntentAction.OPEN_MEDIA\nimport io.github.drumber.kitsune.constants.IntentAction.SHORTCUT_LIBRARY\nimport io.github.drumber.kitsune.constants.IntentAction.SHORTCUT_SEARCH\nimport io.github.drumber.kitsune.constants.IntentAction.SHORTCUT_SETTINGS\nimport io.github.drumber.kitsune.databinding.ActivityMainBinding\nimport io.github.drumber.kitsune.domain.work.UpdateLibraryWidgetUseCase\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.preference.StartPagePref\nimport io.github.drumber.kitsune.preference.getDestinationId\nimport io.github.drumber.kitsune.ui.authentication.AuthenticationActivity\nimport io.github.drumber.kitsune.ui.base.BaseActivity\nimport io.github.drumber.kitsune.ui.details.DetailsFragmentArgs\nimport io.github.drumber.kitsune.ui.details.DetailsFragmentDirections\nimport io.github.drumber.kitsune.ui.onboarding.OnboardingActivity\nimport io.github.drumber.kitsune.ui.permissions.requestNotificationPermission\nimport io.github.drumber.kitsune.ui.permissions.showNotificationPermissionRejectedDialog\nimport io.github.drumber.kitsune.util.extensions.setStatusBarColorRes\nimport io.github.drumber.kitsune.util.ui.RoundBitmapDrawable\nimport io.github.drumber.kitsune.util.ui.getSystemBarsAndCutoutInsets\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport org.koin.android.ext.android.inject\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass MainActivity : BaseActivity() {\n\n    private val viewModel: MainActivityViewModel by viewModel()\n\n    private lateinit var binding: ActivityMainBinding\n\n    private val updateLibraryWidget by inject<UpdateLibraryWidgetUseCase>()\n\n    private lateinit var navController: NavController\n\n    private var overrideStartDestination: Int? = null\n    private var handledIntentHashCode: Int? = null\n\n    private val navigationBarView: NavigationBarView\n        get() = binding.bottomNavigation\n            ?: binding.navigationRail\n            ?: error(\"There must exist a navigation bar view.\")\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback())\n        window.sharedElementsUseOverlay = false\n        super.onCreate(savedInstanceState)\n        binding = ActivityMainBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        lifecycleScope.launch {\n            repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.reLoginPrompt.collectLatest {\n                    promptUserReLogin()\n                }\n            }\n        }\n\n        val initialLoginState = viewModel.isLoggedIn()\n        lifecycleScope.launch {\n            repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.isLoggedInFlow.collectLatest { isLoggedIn ->\n                    if (initialLoginState != isLoggedIn) {\n                        updateLibraryWidget(this@MainActivity)\n                        startNewMainActivity()\n                    }\n                }\n            }\n        }\n\n        val navHostFragment =\n            supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment\n        navHostFragment.childFragmentManager.registerFragmentLifecycleCallbacks(object :\n            FragmentLifecycleCallbacks() {\n            private fun updateDecorationForFragment(fragment: Fragment) {\n                var statusBarColorRes = android.R.color.transparent\n                if (fragment is FragmentDecorationPreference && !fragment.hasTransparentStatusBar) {\n                    statusBarColorRes = R.color.translucent_status_bar\n                }\n                setStatusBarColorRes(statusBarColorRes)\n            }\n\n            override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {\n                updateDecorationForFragment(f)\n            }\n\n            override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {\n                fm.fragments.lastOrNull()?.let { updateDecorationForFragment(it) }\n            }\n        }, true)\n\n        ViewCompat.setOnApplyWindowInsetsListener(binding.navHostFragment) { _, windowInsets ->\n            if (!isNavigationBarViewVisible())\n                return@setOnApplyWindowInsetsListener windowInsets\n\n            val insets = windowInsets.getSystemBarsAndCutoutInsets()\n            val consumedInsets = binding.bottomNavigation?.applyWindowInsets(insets)\n                ?: binding.navigationRail?.applyWindowInsets(insets)\n                ?: Insets.of(0, 0, 0, 0)\n            // consume insets used by the navigation bar view\n            // and propagate the remaining inset space to child fragments\n            windowInsets.inset(consumedInsets)\n        }\n\n        navController = navHostFragment.navController\n        navigationBarView.apply {\n            setOnItemSelectedListener { item ->\n                viewModel.currentNavRootDestId = item.itemId\n\n                // handle reselect of navigation item and pass event to current fragment\n                navHostFragment.childFragmentManager.fragments.let { fragments ->\n                    if (item.itemId == selectedItemId\n                        && fragments.size > 0\n                        && fragments[0] is NavigationBarView.OnItemReselectedListener\n                        && fragments[0].lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)\n                    ) {\n                        (fragments[0] as NavigationBarView.OnItemReselectedListener).onNavigationItemReselected(\n                            item\n                        )\n                    }\n                }\n\n                if (item.itemId == selectedItemId) {\n                    // no need to navigate if we are already at the selected destination\n                    return@setOnItemSelectedListener true\n                }\n\n                // navigate to the target destination\n                NavigationUI.onNavDestinationSelected(item, navController)\n            }\n\n\n            lifecycleScope.launch {\n                repeatOnLifecycle(Lifecycle.State.STARTED) {\n                    viewModel.localUser\n                        .map { it?.avatar?.originalOrDown() }\n                        .distinctUntilChanged()\n                        .collectLatest { avatarUrl ->\n                            if (avatarUrl.isNullOrBlank()) {\n                                menu.findItem(R.id.profile_fragment)\n                                    .setIcon(R.drawable.selector_profile)\n                                return@collectLatest\n                            }\n                            Glide.with(this@MainActivity)\n                                .asBitmap()\n                                .load(avatarUrl)\n                                .dontAnimate()\n                                .into(object : CustomTarget<Bitmap>() {\n                                    override fun onResourceReady(\n                                        resource: Bitmap,\n                                        transition: Transition<in Bitmap>?\n                                    ) {\n                                        menu.findItem(R.id.profile_fragment).icon =\n                                            RoundBitmapDrawable(resource)\n                                    }\n\n                                    override fun onLoadCleared(placeholder: Drawable?) {}\n                                })\n                        }\n                }\n            }\n        }\n\n        navController.addOnDestinationChangedListener { _, destination, _ ->\n            // set the selected bottom navigation item\n            for (menuItem in navigationBarView.menu) {\n                if (menuItem.itemId == destination.id) {\n                    viewModel.currentNavRootDestId = menuItem.itemId\n                }\n                if (menuItem.itemId == viewModel.currentNavRootDestId) {\n                    menuItem.isChecked = true\n                }\n            }\n\n            // hide bottom navigation if the destination is not a main one\n            toggleNavigationBarView(\n                !isDestinationOnMainNavGraph(destination) || destination.id == R.id.webViewFragment,\n                lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)\n            )\n        }\n\n        handledIntentHashCode = when (savedInstanceState?.containsKey(LAST_HANDLED_INTENT_KEY)) {\n            true -> savedInstanceState.getInt(LAST_HANDLED_INTENT_KEY)\n            else -> null\n        }\n\n        if (savedInstanceState == null) {\n            onCreateWithoutSavedInstanceState()\n        }\n    }\n\n    private fun onCreateWithoutSavedInstanceState() {\n        // override start fragment, but only on clean launch and when not launched by a deep link\n        if (!isLaunchedByDeepLink()) {\n            overrideStartDestination = getShortcutStartDestinationId()\n            // if the app wasn't launched from an app shortcut\n            // and the user has specified a custom start page\n            // then set the start fragment to the custom one\n            if (overrideStartDestination == null && KitsunePref.startFragment != StartPagePref.Home) {\n                overrideStartDestination = KitsunePref.startFragment.getDestinationId()\n            }\n        }\n\n        if (shouldStartOnboarding()) {\n            startOnboardingActivity()\n        } else if (KitsunePref.checkForUpdatesOnStart) {\n            requestRequiredPermissions()\n        }\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        handledIntentHashCode?.let { outState.putInt(LAST_HANDLED_INTENT_KEY, it) }\n    }\n\n    override fun onStart() {\n        super.onStart()\n        if (!handleIntentAction(intent)) {\n            overrideStartDestination?.let {\n                navigateToSingleTopDestination(it)\n                overrideStartDestination = null\n            }\n        }\n    }\n\n    private fun requestRequiredPermissions() {\n        if (!KitsunePref.flagUserDeniedNotificationPermission) {\n            val requestNotificationPermissionLauncher =\n                registerForActivityResult(RequestPermission()) { isGranted ->\n                    if (isGranted) {\n                        KitsunePref.flagUserDeniedNotificationPermission = false\n                    } else {\n                        KitsunePref.checkForUpdatesOnStart = false\n                        KitsunePref.flagUserDeniedNotificationPermission = true\n                        showNotificationPermissionRejectedDialog()\n                    }\n                }\n            requestNotificationPermission(requestNotificationPermissionLauncher) {\n                KitsunePref.flagUserDeniedNotificationPermission = true\n            }\n        }\n    }\n\n    private fun promptUserReLogin() {\n        val intent = Intent(this, AuthenticationActivity::class.java)\n        intent.putExtra(AuthenticationActivity.EXTRA_LOGGED_OUT, true)\n        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP\n        startActivity(intent)\n    }\n\n    private fun startNewMainActivity() {\n        val intent = Intent(this, MainActivity::class.java)\n        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)\n        startActivity(intent)\n    }\n\n    private fun shouldStartOnboarding(): Boolean {\n        return !BuildConfig.INSTRUMENTED_TEST && KitsunePref.onboardingFinishedVersionCode == -1\n    }\n\n    private fun startOnboardingActivity() {\n        val intent = Intent(this, OnboardingActivity::class.java)\n        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)\n        startActivity(intent)\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        return navController.navigateUp() || super.onSupportNavigateUp()\n    }\n\n    /** Checks if the activity was launched using an app link, */\n    private fun isLaunchedByDeepLink(): Boolean {\n        return intent.action == Intent.ACTION_VIEW && intent.data != null\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        if (!navController.handleDeepLink(intent)) {\n            handleIntentAction(intent)\n        }\n    }\n\n    private fun handleIntentAction(intent: Intent): Boolean {\n        if (handledIntentHashCode == intent.filterHashCode()) return false\n        handledIntentHashCode = intent.filterHashCode()\n\n        return when (intent.action) {\n            OPEN_MEDIA -> {\n                val argsResult = intent.extras?.runCatching {\n                    DetailsFragmentArgs.fromBundle(this)\n                }\n                argsResult?.getOrNull()?.let { args ->\n                    val action = DetailsFragmentDirections.actionGlobalDetailsFragment(\n                        media = args.media,\n                        type = args.type,\n                        slug = args.slug\n                    )\n                    navController.navigate(action)\n                    true\n                } ?: false\n            }\n\n            OPEN_LIBRARY -> {\n                navigateToSingleTopDestination(R.id.library_fragment)\n                true\n            }\n\n            else -> false\n        }\n    }\n\n    private fun getShortcutStartDestinationId(): Int? {\n        return when (intent.action) {\n            SHORTCUT_LIBRARY -> R.id.library_fragment\n            SHORTCUT_SEARCH -> R.id.search_fragment\n            SHORTCUT_SETTINGS -> R.id.settings_nav_graph\n            else -> null\n        }\n    }\n\n    private fun navigateToSingleTopDestination(navigationId: Int) {\n        val navOptions = NavOptions.Builder()\n            .setLaunchSingleTop(true)\n            .setRestoreState(true)\n            .setPopUpTo(R.id.main_fragment, inclusive = false, saveState = true)\n            .build()\n        navController.navigate(navigationId, null, navOptions)\n    }\n\n    private fun isDestinationOnMainNavGraph(destination: NavDestination): Boolean {\n        return destination.parent?.id == R.id.main_nav_graph\n    }\n\n    private fun isNavigationBarViewVisible(): Boolean {\n        return navController.currentDestination\n            ?.let { isDestinationOnMainNavGraph(it) } ?: true\n    }\n\n    private fun toggleNavigationBarView(hideNavigationBar: Boolean, animate: Boolean = true) {\n        if (!animate) {\n            navigationBarView.isVisible = !hideNavigationBar\n        } else {\n            when {\n                binding.bottomNavigation != null -> animateBottomNavigation(hideNavigationBar)\n                binding.navigationRail != null -> animateNavigationRail(hideNavigationBar)\n            }\n        }\n    }\n\n    private fun animateBottomNavigation(slideDown: Boolean) {\n        binding.bottomNavigation?.apply {\n            if (slideDown) {\n                animate().translationY(this.height.toFloat())\n                    .withEndAction { this.isVisible = false }\n                    .duration =\n                    resources.getInteger(R.integer.bottom_navigation_animation_duration)\n                        .toLong()\n            } else {\n                animate().translationY(0f)\n                    .withStartAction { this.isVisible = true }\n                    .duration =\n                    resources.getInteger(R.integer.bottom_navigation_animation_duration)\n                        .toLong()\n            }\n        }\n    }\n\n    private fun animateNavigationRail(slideOut: Boolean) {\n        binding.navigationRail?.apply {\n            if (slideOut) {\n                // different direction depending on if rail is left or right aligned\n                val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL\n                val translationFactor = if (isRtl) 1 else -1\n                animate().translationX(this.width.toFloat() * translationFactor)\n                    .withEndAction { this.isVisible = false }\n                    .duration =\n                    resources.getInteger(R.integer.navigation_rail_animation_duration)\n                        .toLong()\n            } else {\n                animate().translationX(0f)\n                    .withStartAction { this.isVisible = true }\n                    .duration =\n                    resources.getInteger(R.integer.navigation_rail_animation_duration)\n                        .toLong()\n            }\n        }\n    }\n\n    private fun BottomNavigationView.applyWindowInsets(insets: Insets): Insets {\n        updatePadding(left = insets.left, right = insets.right, bottom = insets.bottom)\n        return Insets.of(0, 0, 0, insets.bottom)\n    }\n\n    private fun NavigationRailView.applyWindowInsets(insets: Insets): Insets {\n        val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL\n        val left = if (!isRtl) insets.left else 0\n        val right = if (isRtl) insets.right else 0\n        updatePadding(left = left, top = insets.top, right = right, bottom = insets.bottom)\n        return Insets.of(left, 0, right, 0)\n    }\n\n    companion object {\n        private const val LAST_HANDLED_INTENT_KEY = \"last_handled_intent\"\n    }\n}\n\ninterface FragmentDecorationPreference {\n    val hasTransparentStatusBar: Boolean\n        get() = true\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/main/MainActivityViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.main\n\nimport androidx.annotation.IdRes\nimport androidx.lifecycle.ViewModel\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository.AccessTokenState\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport kotlinx.coroutines.flow.map\n\nclass MainActivityViewModel(\n    userRepository: UserRepository,\n    private val accessTokenRepository: AccessTokenRepository\n) : ViewModel() {\n\n    /** Destination ID of the current selected bottom navigation item. */\n    @IdRes\n    var currentNavRootDestId: Int = R.id.main_fragment\n\n    val reLoginPrompt = userRepository.userReLogInPrompt\n\n    val localUser = userRepository.localUser\n\n    val isLoggedInFlow =\n        accessTokenRepository.accessTokenState.map { it == AccessTokenState.PRESENT }\n\n    fun isLoggedIn() = accessTokenRepository.accessTokenState.value == AccessTokenState.PRESENT\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/main/MainFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.main\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.doOnPreDraw\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport androidx.navigation.fragment.FragmentNavigatorExtras\nimport androidx.navigation.fragment.findNavController\nimport com.google.android.material.navigation.NavigationBarView\nimport com.google.android.material.shape.MaterialShapeDrawable\nimport com.google.android.material.tabs.TabLayoutMediator\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.FragmentMainBinding\nimport io.github.drumber.kitsune.ui.main.MainFragmentViewModel.NavigationAction\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.extensions.recyclerView\nimport io.github.drumber.kitsune.util.extensions.setAppTheme\nimport io.github.drumber.kitsune.util.ui.initMarginWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.navigation.koinNavGraphViewModel\n\nclass MainFragment : Fragment(R.layout.fragment_main), NavigationBarView.OnItemReselectedListener {\n\n    private var _binding: FragmentMainBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: MainFragmentViewModel by koinNavGraphViewModel(R.id.main_nav_graph)\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentMainBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        postponeEnterTransition()\n        view.doOnPreDraw { startPostponedEnterTransition() }\n        initExploreViewPager()\n\n        binding.appBarLayout.statusBarForeground =\n            MaterialShapeDrawable.createWithElevationOverlay(context)\n        binding.toolbar.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            consume = false\n        )\n        binding.tabLayoutExplore.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            consume = false\n        )\n        binding.swipeRefreshLayout.initMarginWindowInsetsListener(\n            left = true,\n            right = true,\n            consume = false\n        )\n        binding.nsvContent.initPaddingWindowInsetsListener(bottom = true, consume = false)\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.reloadFinished.collect { isReloadFinished ->\n                    binding.swipeRefreshLayout.isRefreshing = !isReloadFinished\n                }\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.navigationAction.collect(::handleNavigationAction)\n            }\n        }\n    }\n\n    private fun handleNavigationAction(navigationAction: NavigationAction) {\n        when (navigationAction) {\n            is NavigationAction.OpenMediaList -> {\n                val action =\n                    MainFragmentDirections.actionMainFragmentToMediaListFragment(navigationAction.mediaSelector, navigationAction.title)\n                findNavController().navigateSafe(R.id.main_fragment, action)\n            }\n\n            is NavigationAction.OpenMediaDetails -> {\n                val action = MainFragmentDirections.actionMainFragmentToDetailsFragment(navigationAction.mediaDto)\n                val detailsTransitionName = getString(R.string.details_poster_transition_name)\n                val extras = FragmentNavigatorExtras(navigationAction.sharedElement to detailsTransitionName)\n                findNavController().navigateSafe(R.id.main_fragment, action, extras)\n            }\n        }\n    }\n\n    private fun initExploreViewPager() {\n        binding.viewPagerExplore.apply {\n            adapter = HomeExploreViewPagerAdapter(this@MainFragment)\n            recyclerView.isNestedScrollingEnabled = false\n        }\n\n        TabLayoutMediator(binding.tabLayoutExplore, binding.viewPagerExplore) { tab, position ->\n            when (position) {\n                0 -> {\n                    tab.text = getString(R.string.anime)\n                }\n\n                1 -> {\n                    tab.text = getString(R.string.manga)\n                }\n            }\n        }.attach()\n\n        binding.swipeRefreshLayout.apply {\n            setAppTheme()\n            isRefreshing = isRefreshing && viewModel.isSomeEntryReloading()\n\n            setOnRefreshListener {\n                when (binding.viewPagerExplore.currentItem) {\n                    0 -> viewModel.refreshAnimeData()\n                    1 -> viewModel.refreshMangaData()\n                }\n            }\n        }\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        binding.nsvContent.smoothScrollTo(0, 0)\n        binding.appBarLayout.setExpanded(true)\n    }\n\n    override fun onDestroyView() {\n        _binding?.viewPagerExplore?.adapter = null\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/main/MainFragmentViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.main\n\nimport android.view.View\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.liveData\nimport androidx.lifecycle.switchMap\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.constants.Defaults\nimport io.github.drumber.kitsune.constants.SortFilter\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.presentation.dto.MediaDto\nimport io.github.drumber.kitsune.data.presentation.model.media.MediaSelector\nimport io.github.drumber.kitsune.data.presentation.model.media.identifier\nimport io.github.drumber.kitsune.data.repository.AnimeRepository\nimport io.github.drumber.kitsune.data.repository.MangaRepository\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.logV\nimport io.github.drumber.kitsune.util.network.ResponseData\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nclass MainFragmentViewModel(\n    private val animeRepository: AnimeRepository,\n    private val mangaRepository: MangaRepository\n) : ViewModel() {\n\n    private val _navigationAction = MutableSharedFlow<NavigationAction>()\n    val navigationAction = _navigationAction.asSharedFlow()\n\n    suspend fun navigate(action: NavigationAction) = withContext(Dispatchers.Main) {\n        _navigationAction.emit(action)\n    }\n\n    private val animeReload = MutableLiveData(Any())\n    private val mangaReload = MutableLiveData(Any())\n\n    // Contains a boolean value for each explore section type which represents\n    // whether the entry is reloading from network or not.\n    private var animeReloadMap = mutableMapOf<String, Boolean>()\n    private var mangaReloadMap = mutableMapOf<String, Boolean>()\n\n    private val _reloadFinished = MutableSharedFlow<Boolean>()\n    val reloadFinished = _reloadFinished.asSharedFlow()\n\n    private val animeExploreSections = mutableMapOf(\n        // trending\n        createAnimeExploreEntry(TRENDING) {\n            animeRepository.getTrending(Filter().limit(10))\n        },\n        // top airing\n        createAnimeExploreEntry(TOP_AIRING) {\n            animeRepository.getAllAnime(FILTER_TOP_AIRING_ANIME)\n        },\n        // top upcoming\n        createAnimeExploreEntry(TOP_UPCOMING) {\n            animeRepository.getAllAnime(FILTER_TOP_UPCOMING_ANIME)\n        },\n        // highest rated\n        createAnimeExploreEntry(HIGHEST_RATED) {\n            animeRepository.getAllAnime(FILTER_HIGHEST_RATED_ANIME)\n        },\n        // most popular\n        createAnimeExploreEntry(MOST_POPULAR) {\n            animeRepository.getAllAnime(FILTER_MOST_POPULAR_ANIME)\n        }\n    )\n\n    private val mangaExploreSections = mutableMapOf(\n        // trending\n        createMangaExploreEntry(TRENDING) {\n            mangaRepository.getTrending(Filter().limit(10))\n        },\n        // top airing\n        createMangaExploreEntry(TOP_AIRING) {\n            mangaRepository.getAllManga(FILTER_TOP_AIRING_MANGA)\n        },\n        // top upcoming\n        createMangaExploreEntry(TOP_UPCOMING) {\n            mangaRepository.getAllManga(FILTER_TOP_UPCOMING_MANGA)\n        },\n        // highest rated\n        createMangaExploreEntry(HIGHEST_RATED) {\n            mangaRepository.getAllManga(FILTER_HIGHEST_RATED_MANGA)\n        },\n        // most popular\n        createMangaExploreEntry(MOST_POPULAR) {\n            mangaRepository.getAllManga(FILTER_MOST_POPULAR_MANGA)\n        }\n    )\n\n    fun getAnimeExploreLiveData(type: String) = animeExploreSections[type]\n        ?: throw IllegalArgumentException(\"There is no anime live data for type ''$type.\")\n\n    fun getMangaExploreLiveData(type: String) = mangaExploreSections[type]\n        ?: throw IllegalArgumentException(\"There is no manga live data for type ''$type.\")\n\n    fun refreshAnimeData() {\n        if (!animeReloadMap.containsValue(true)) {\n            animeReload.postValue(Any())\n        }\n    }\n\n    fun refreshMangaData() {\n        if (!mangaReloadMap.containsValue(true)) {\n            mangaReload.postValue(Any())\n        }\n    }\n\n    private fun <T> createAnimeExploreEntry(key: String, call: suspend () -> List<T>?) = Pair(\n        key,\n        animeReload.switchMap {\n            liveData(Dispatchers.IO) {\n                animeReloadMap[key] = true\n                val responseData = processCall(call)\n                emit(responseData)\n                animeReloadMap[key] = false\n                onEntryReloadFinished()\n            }\n        }\n    ).also { animeReloadMap[key] = false }\n\n    private fun <T> createMangaExploreEntry(key: String, call: suspend () -> List<T>?) = Pair(\n        key,\n        mangaReload.switchMap {\n            liveData(Dispatchers.IO) {\n                mangaReloadMap[key] = true\n                val responseData = processCall(call)\n                mangaReloadMap[key] = false\n                onEntryReloadFinished()\n                emit(responseData)\n            }\n        }\n    ).also { mangaReloadMap[key] = false }\n\n    private fun onEntryReloadFinished() {\n        logV(\"isSomeEntryReloading: ${isSomeEntryReloading()} \" +\n                \"Remaining: \" +\n                \"Anime: \" + animeReloadMap.count { it.value } +\n                \" Manga: \" + mangaReloadMap.count { it.value }\n        )\n        if (!isSomeEntryReloading()) {\n            viewModelScope.launch(Dispatchers.Main) {\n                _reloadFinished.emit(true)\n            }\n        }\n    }\n\n    fun isSomeEntryReloading(): Boolean {\n        return animeReloadMap.containsValue(true) || mangaReloadMap.containsValue(true)\n    }\n\n    private suspend fun <T> processCall(call: suspend () -> List<T>?): ResponseData<List<T>> {\n        return try {\n            val data = call() ?: throw NoDataException(\"Received data is 'null'.\")\n            ResponseData.Success(data)\n        } catch (e: Exception) {\n            logE(\"Failed to load data.\", e)\n            ResponseData.Error(e)\n        }\n    }\n\n    sealed interface NavigationAction {\n        data class OpenMediaList(val mediaSelector: MediaSelector, val title: String) : NavigationAction\n        data class OpenMediaDetails(val mediaDto: MediaDto, val sharedElement: View) : NavigationAction\n    }\n\n    companion object {\n        const val TRENDING = \"anime_trending\"\n        const val TOP_AIRING = \"top_airing\"\n        const val TOP_UPCOMING = \"top_upcoming\"\n        const val HIGHEST_RATED = \"highest_rated\"\n        const val MOST_POPULAR = \"most_popular\"\n\n        val FILTER_TOP_AIRING_ANIME get() = createFilter(MediaType.Anime, \"current\")\n        val FILTER_TOP_UPCOMING_ANIME get() = createFilter(MediaType.Anime, \"upcoming\")\n        val FILTER_HIGHEST_RATED_ANIME get() = createFilter(MediaType.Anime, sortBy = SortFilter.AVERAGE_RATING_DESC)\n        val FILTER_MOST_POPULAR_ANIME get() = createFilter(MediaType.Anime, sortBy = SortFilter.POPULARITY_DESC)\n\n        val FILTER_TOP_AIRING_MANGA get() = createFilter(MediaType.Manga, \"current\")\n        val FILTER_TOP_UPCOMING_MANGA get() = createFilter(MediaType.Manga, \"upcoming\")\n        val FILTER_HIGHEST_RATED_MANGA get() = createFilter(MediaType.Manga, sortBy = SortFilter.AVERAGE_RATING_DESC)\n        val FILTER_MOST_POPULAR_MANGA get() = createFilter(MediaType.Manga, sortBy = SortFilter.POPULARITY_DESC)\n\n        private fun createFilter(\n            type: MediaType,\n            filterType: String? = null,\n            sortBy: SortFilter = SortFilter.POPULARITY_DESC\n        ) = Filter().apply {\n            pageLimit(10)\n            filterType?.let { filter(\"status\", it) }\n            sort(sortBy.queryParam)\n            fields(type.identifier, *Defaults.MINIMUM_COLLECTION_FIELDS)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/medialist/MediaListFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.medialist\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.doOnPreDraw\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport androidx.navigation.fragment.FragmentNavigatorExtras\nimport androidx.navigation.fragment.findNavController\nimport androidx.navigation.fragment.navArgs\nimport androidx.paging.LoadState\nimport androidx.paging.PagingData\nimport androidx.paging.PagingDataAdapter\nimport com.bumptech.glide.Glide\nimport com.google.android.material.color.MaterialColors\nimport com.google.android.material.navigation.NavigationBarView\nimport com.google.android.material.transition.MaterialSharedAxis\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaDto\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.databinding.FragmentMediaListBinding\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.adapter.paging.AnimeAdapter\nimport io.github.drumber.kitsune.ui.adapter.paging.MangaAdapter\nimport io.github.drumber.kitsune.ui.adapter.paging.ResourceLoadStateAdapter\nimport io.github.drumber.kitsune.ui.component.LoadStateSpanSizeLookup\nimport io.github.drumber.kitsune.ui.component.ResponsiveGridLayoutManager\nimport io.github.drumber.kitsune.ui.component.updateLoadState\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass MediaListFragment : Fragment(R.layout.fragment_media_list),\n    NavigationBarView.OnItemReselectedListener {\n\n    private val args: MediaListFragmentArgs by navArgs()\n\n    private var _binding: FragmentMediaListBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: MediaListViewModel by viewModel()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)\n        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentMediaListBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        postponeEnterTransition()\n\n        if (findNavController().currentBackStackEntry?.arguments == null) {\n            view.doOnPreDraw { startPostponedEnterTransition() }\n        } else {\n            // safeguard: ensure startPostponedEnterTransition() got called within 200ms\n            view.postDelayed({\n                startPostponedEnterTransition()\n            }, 200)\n        }\n\n        val colorBackground = MaterialColors.getColor(view, android.R.attr.colorBackground)\n        view.setBackgroundColor(colorBackground)\n\n        binding.rvMedia.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            bottom = true,\n            consume = false\n        )\n\n        viewModel.setMediaSelector(args.mediaSelector)\n\n        binding.collapsingToolbar.initWindowInsetsListener(consume = false)\n        binding.toolbar.apply {\n            initWindowInsetsListener(consume = false)\n            title = args.title\n            setNavigationOnClickListener { findNavController().navigateUp() }\n        }\n\n        initRecyclerView()\n    }\n\n    private fun initRecyclerView() {\n        val glide = Glide.with(this)\n\n        val adapter = when (args.mediaSelector.mediaType) {\n            MediaType.Anime -> AnimeAdapter(glide, this::onMediaClicked)\n            MediaType.Manga -> MangaAdapter(glide, this::onMediaClicked)\n        } as PagingDataAdapter<Media, *>\n\n        val columnWidth = resources.getDimension(KitsunePref.mediaItemSize.widthRes) +\n                2 * resources.getDimension(R.dimen.media_item_margin)\n        val gridLayout = ResponsiveGridLayoutManager(requireContext(), columnWidth.toInt(), 2)\n        gridLayout.spanSizeLookup = LoadStateSpanSizeLookup(adapter, gridLayout)\n\n        binding.rvMedia.adapter = adapter.withLoadStateHeaderAndFooter(\n            header = ResourceLoadStateAdapter(adapter),\n            footer = ResourceLoadStateAdapter(adapter)\n        )\n        binding.rvMedia.layoutManager = gridLayout\n\n        binding.layoutLoading.btnRetry.setOnClickListener { adapter.retry() }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                adapter.loadStateFlow.collectLatest { loadStates ->\n                    binding.layoutLoading.updateLoadState(\n                        binding.rvMedia,\n                        adapter.itemCount,\n                        loadStates\n                    )\n\n                    if (loadStates.refresh is LoadState.NotLoading) {\n                        binding.rvMedia.doOnPreDraw { startPostponedEnterTransition() }\n                    }\n                }\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.dataSource.collectLatest { data ->\n                    adapter.submitData(data as PagingData<Media>)\n                }\n            }\n        }\n    }\n\n    fun onMediaClicked(view: View, model: Media) {\n        val action =\n            MediaListFragmentDirections.actionMediaListFragmentToDetailsFragment(model.toMediaDto())\n        val detailsTransitionName = getString(R.string.details_poster_transition_name)\n        val extras = FragmentNavigatorExtras(view to detailsTransitionName)\n        findNavController().navigateSafe(R.id.media_list_fragment, action, extras)\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        if (binding.rvMedia.canScrollVertically(-1)) {\n            binding.rvMedia.smoothScrollToPosition(0)\n            binding.appBarLayout.setExpanded(true)\n        } else {\n            findNavController().navigateUp()\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/medialist/MediaListViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.medialist\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.PagingData\nimport androidx.paging.cachedIn\nimport io.github.drumber.kitsune.constants.Defaults\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.FilterOptions\nimport io.github.drumber.kitsune.data.common.media.MediaType\nimport io.github.drumber.kitsune.data.common.toFilter\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.presentation.model.media.MediaSelector\nimport io.github.drumber.kitsune.data.presentation.model.media.RequestType\nimport io.github.drumber.kitsune.data.presentation.model.media.identifier\nimport io.github.drumber.kitsune.data.repository.AnimeRepository\nimport io.github.drumber.kitsune.data.repository.MangaRepository\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.filterNotNull\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\n\nclass MediaListViewModel(\n    private val animeRepository: AnimeRepository,\n    private val mangaRepository: MangaRepository\n) : ViewModel() {\n\n    private val mediaSelectorFlow: StateFlow<MediaSelector?>\n\n    val setMediaSelector: (MediaSelector) -> Unit\n\n    init {\n        val mutableMediaSelectorFlow = MutableSharedFlow<MediaSelector>(replay = 1)\n\n        mediaSelectorFlow = mutableMediaSelectorFlow\n            .distinctUntilChanged()\n            .stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.Lazily,\n                initialValue = null\n            )\n\n        setMediaSelector = { mediaSelector ->\n            viewModelScope.launch {\n                mutableMediaSelectorFlow.emit(mediaSelector)\n            }\n        }\n    }\n\n    @OptIn(ExperimentalCoroutinesApi::class)\n    val dataSource: Flow<PagingData<out Media>> = mediaSelectorFlow\n        .filterNotNull()\n        .distinctUntilChanged()\n        .flatMapLatest { selector ->\n            // copy the filter and limit the fields of the response model to only the required ones\n            val mediaSelector = with(selector) {\n                copy(\n                    filterOptions = Filter(filterOptions.toMutableMap())\n                        .fields(mediaType.identifier, *Defaults.MINIMUM_COLLECTION_FIELDS)\n                        .options\n                )\n            }\n            getData(mediaSelector)\n        }.cachedIn(viewModelScope)\n\n    private fun getData(mediaSelector: MediaSelector): Flow<PagingData<Media>> {\n        return when (mediaSelector.mediaType) {\n            MediaType.Anime -> getAnimeData(mediaSelector.requestType, mediaSelector.filterOptions)\n            MediaType.Manga -> getMangaData(mediaSelector.requestType, mediaSelector.filterOptions)\n        } as Flow<PagingData<Media>>\n    }\n\n    private fun getAnimeData(type: RequestType, filterOptions: FilterOptions) = when (type) {\n        RequestType.ALL -> animeRepository.animePager(\n            filterOptions.toFilter(),\n            Kitsu.DEFAULT_PAGE_SIZE\n        )\n\n        RequestType.TRENDING -> animeRepository.trendingAnimePager(\n            filterOptions.toFilter(),\n            Kitsu.DEFAULT_PAGE_SIZE\n        )\n    }\n\n    private fun getMangaData(type: RequestType, filterOptions: FilterOptions) = when (type) {\n        RequestType.ALL -> mangaRepository.mangaPager(\n            filterOptions.toFilter(),\n            Kitsu.DEFAULT_PAGE_SIZE\n        )\n\n        RequestType.TRENDING -> mangaRepository.trendingMangaPager(\n            filterOptions.toFilter(),\n            Kitsu.DEFAULT_PAGE_SIZE\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/OnboardingActivity.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding\n\nimport android.content.Intent\nimport android.graphics.Color\nimport android.net.Uri\nimport android.os.Bundle\nimport androidx.activity.SystemBarStyle\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.PagerState\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Info\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.asFlow\nimport com.chibatching.kotpref.livedata.asLiveData\nimport io.github.drumber.kitsune.BuildConfig\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.authentication.AuthenticationActivity\nimport io.github.drumber.kitsune.ui.base.BaseActivity\nimport io.github.drumber.kitsune.ui.onboarding.pages.LoginPage\nimport io.github.drumber.kitsune.ui.onboarding.pages.SetupPageAdapter\nimport io.github.drumber.kitsune.ui.onboarding.pages.WelcomePage\nimport io.github.drumber.kitsune.ui.theme.KitsuneTheme\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass OnboardingActivity : BaseActivity() {\n\n    private val viewModel: OnboardingViewModel by viewModel()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enableEdgeToEdge(\n            navigationBarStyle = SystemBarStyle.auto(\n                Color.TRANSPARENT,\n                Color.TRANSPARENT\n            )\n        )\n\n        setContent {\n            val useDynamicColorTheme by KitsunePref.asLiveData(KitsunePref::useDynamicColorTheme)\n                .asFlow()\n                .collectAsState(initial = KitsunePref.useDynamicColorTheme)\n            val darkModePreference by KitsunePref.asLiveData(KitsunePref::darkMode)\n                .asFlow()\n                .collectAsState(initial = KitsunePref.darkMode)\n\n            val isDarkModeEnabled = when (darkModePreference.toInt()) {\n                AppCompatDelegate.MODE_NIGHT_NO -> false\n                AppCompatDelegate.MODE_NIGHT_YES -> true\n                else -> isSystemInDarkTheme()\n            }\n\n            val uiState by viewModel.uiSate.collectAsState()\n            val localUser by viewModel.localUser.collectAsState()\n\n            var openCreateAccountForwardDialog by remember { mutableStateOf(false) }\n\n            KitsuneTheme(dynamicColor = useDynamicColorTheme, darkTheme = isDarkModeEnabled) {\n                Scaffold(\n                    modifier = Modifier.fillMaxSize(),\n                    contentWindowInsets = WindowInsets.safeDrawing\n                ) { innerPadding ->\n                    OnboardingTour(\n                        uiState = uiState,\n                        localUser = localUser,\n                        contentPadding = innerPadding,\n                        onNavigateToLogin = {\n                            startActivity(Intent(this, AuthenticationActivity::class.java))\n                        },\n                        onNavigateToCreateAccount = {\n                            openCreateAccountForwardDialog = true\n                        },\n                        onFinish = {\n                            KitsunePref.onboardingFinishedVersionCode = BuildConfig.VERSION_CODE\n                            finish()\n                        }\n                    )\n                    if (openCreateAccountForwardDialog) {\n                        CreateAccountForwardDialog(\n                            onDismissRequest = { openCreateAccountForwardDialog = false },\n                            onConfirmation = {\n                                openCreateAccountForwardDialog = false\n                                startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(Kitsu.BASE_URL)))\n                            }\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun OnboardingTour(\n    uiState: OnboardingUiState = OnboardingUiState(),\n    localUser: LocalUser? = null,\n    contentPadding: PaddingValues = PaddingValues(0.dp),\n    onNavigateToLogin: () -> Unit = {},\n    onNavigateToCreateAccount: () -> Unit = {},\n    onFinish: () -> Unit = {}\n) {\n    val pagerState = rememberPagerState(pageCount = { 3 })\n    val contentPaddingWithoutBottom = PaddingValues(\n        start = contentPadding.calculateStartPadding(LocalLayoutDirection.current),\n        top = contentPadding.calculateTopPadding(),\n        end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),\n        bottom = 0.dp\n    )\n    val contentPaddingWithoutTop = PaddingValues(\n        start = contentPadding.calculateStartPadding(LocalLayoutDirection.current),\n        top = 0.dp,\n        end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),\n        bottom = contentPadding.calculateBottomPadding()\n    )\n\n    val coroutineScope = rememberCoroutineScope()\n\n    Surface(\n        color = MaterialTheme.colorScheme.surface\n    ) {\n        Column(modifier = Modifier) {\n            HorizontalPager(\n                state = pagerState,\n                modifier = Modifier.weight(1f)\n            ) { page ->\n                when (page) {\n                    0 -> WelcomePage(\n                        modifier = Modifier.padding(contentPaddingWithoutBottom),\n                        uiState = uiState,\n                        onNextClicked = {\n                            coroutineScope.launch { pagerState.animateScrollToPage(1) }\n                        }\n                    )\n\n                    1 -> LoginPage(\n                        modifier = Modifier.padding(contentPaddingWithoutBottom),\n                        localUser = localUser,\n                        onBack = {\n                            coroutineScope.launch { pagerState.animateScrollToPage(0) }\n                        },\n                        onNext = {\n                            coroutineScope.launch { pagerState.animateScrollToPage(2) }\n                        },\n                        onLoginClicked = onNavigateToLogin,\n                        onCreateAccountClicked = onNavigateToCreateAccount\n                    )\n\n                    2 -> SetupPageAdapter(\n                        modifier = Modifier.padding(contentPaddingWithoutBottom),\n                        localUser = localUser,\n                        onBack = {\n                            coroutineScope.launch { pagerState.animateScrollToPage(1) }\n                        },\n                        onFinishClicked = onFinish\n                    )\n                }\n            }\n            PageIndicator(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(contentPaddingWithoutTop),\n                pagerState = pagerState\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun PageIndicator(modifier: Modifier, pagerState: PagerState) {\n    Row(\n        modifier\n            .wrapContentHeight()\n            .padding(bottom = 8.dp),\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.Bottom\n    ) {\n        repeat(pagerState.pageCount) { page ->\n            val color = if (pagerState.currentPage == page) {\n                MaterialTheme.colorScheme.primary\n            } else {\n                MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)\n            }\n            Box(\n                Modifier\n                    .padding(4.dp)\n                    .clip(CircleShape)\n                    .background(color)\n                    .size(8.dp)\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun CreateAccountForwardDialog(\n    onDismissRequest: () -> Unit,\n    onConfirmation: () -> Unit\n) {\n    AlertDialog(\n        title = { Text(text = stringResource(R.string.onboarding_login_forward_dialog_title)) },\n        text = {\n            Text(\n                text = stringResource(\n                    R.string.onboarding_login_forward_dialog_message,\n                    Kitsu.API_HOST\n                )\n            )\n        },\n        icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) },\n        onDismissRequest = onDismissRequest,\n        confirmButton = {\n            TextButton(onClick = onConfirmation) {\n                Text(text = stringResource(R.string.action_ok))\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismissRequest) {\n                Text(text = stringResource(R.string.action_cancel))\n            }\n        }\n    )\n}\n\n@Preview(showBackground = true)\n@Composable\nprivate fun OnboardingTourPreview() {\n    KitsuneTheme {\n        OnboardingTour()\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nprivate fun CreateAccountForwardDialogPreview() {\n    KitsuneTheme {\n        CreateAccountForwardDialog({}, {})\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/OnboardingUiState.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding\n\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\n\ndata class OnboardingUiState(\n    val backgroundImages: List<String> = emptyList(),\n    val user: LocalUser? = null\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/OnboardingViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.repository.AnimeRepository\nimport io.github.drumber.kitsune.data.repository.MangaRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass OnboardingViewModel(\n    private val animeRepository: AnimeRepository,\n    private val mangaRepository: MangaRepository,\n    userRepository: UserRepository\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(OnboardingUiState())\n    val uiSate = _uiState.asStateFlow()\n\n    val localUser = userRepository.localUser\n\n    init {\n        loadPosterImages()\n    }\n\n    private fun loadPosterImages() {\n        val baseFilter = Filter()\n            .sort(\"-userCount\")\n            .fields(\"media\", \"posterImage\")\n            .pageLimit(10)\n\n        val animeRandomOffset = (0..20).random()\n        val mangaRandomOffset = (0..20).random()\n\n        viewModelScope.launch(Dispatchers.IO) {\n            val submitPosterImages = { posterImages: List<String> ->\n                _uiState.update { it.copy(backgroundImages = it.backgroundImages + posterImages) }\n            }\n\n            launch {\n                fetchPosterImages {\n                    animeRepository.getAllAnime(baseFilter.copy().pageOffset(animeRandomOffset))\n                }?.let { posterImages ->\n                    submitPosterImages(posterImages)\n                }\n            }\n            launch {\n                fetchPosterImages {\n                    mangaRepository.getAllManga(baseFilter.copy().pageOffset(mangaRandomOffset))\n                }?.let { posterImages ->\n                    submitPosterImages(posterImages)\n                }\n            }\n        }\n    }\n\n    private suspend fun fetchPosterImages(request: suspend () -> List<Media>?): List<String>? {\n        return try {\n            request()?.mapNotNull { it.posterImage?.largeOrDown() }\n        } catch (e: Exception) {\n            logE(\"Failed to request poster images.\", e)\n            null\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/components/CustomDialog.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ExperimentalLayoutApi\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialogDefaults\nimport androidx.compose.material3.BasicAlertDialog\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)\n@Composable\nfun CustomDialog(\n    modifier: Modifier = Modifier,\n    icon: @Composable (() -> Unit)? = null,\n    title: (@Composable () -> Unit)? = null,\n    onDismissRequest: () -> Unit,\n    confirmButton: @Composable () -> Unit,\n    dismissButton: @Composable (() -> Unit)?,\n    shape: Shape = AlertDialogDefaults.shape,\n    containerColor: Color = AlertDialogDefaults.containerColor,\n    iconContentColor: Color = AlertDialogDefaults.iconContentColor,\n    titleContentColor: Color = AlertDialogDefaults.titleContentColor,\n    textContentColor: Color = AlertDialogDefaults.textContentColor,\n    tonalElevation: Dp = AlertDialogDefaults.TonalElevation,\n    properties: DialogProperties = DialogProperties(),\n    content: @Composable (contentPadding: PaddingValues) -> Unit\n) {\n    val horizontalDialogPadding = PaddingValues(\n        start = DialogPadding.calculateStartPadding(LocalLayoutDirection.current),\n        end = DialogPadding.calculateEndPadding(LocalLayoutDirection.current)\n    )\n\n    val contentScrollState = rememberScrollState()\n\n    BasicAlertDialog(\n        onDismissRequest = onDismissRequest,\n        modifier = modifier,\n        properties = properties\n    ) {\n        Surface(\n            shape = shape,\n            color = containerColor,\n            tonalElevation = tonalElevation,\n        ) {\n            Column(\n                modifier = Modifier.padding(\n                    top = DialogPadding.calculateTopPadding(),\n                    bottom = DialogPadding.calculateBottomPadding()\n                )\n            ) {\n                // Icon\n                icon?.let {\n                    CompositionLocalProvider(LocalContentColor provides iconContentColor) {\n                        Box(\n                            modifier = Modifier\n                                .padding(horizontalDialogPadding)\n                                .padding(IconPadding)\n                                .align(Alignment.CenterHorizontally)\n                        ) {\n                            icon()\n                        }\n                    }\n                }\n                // Title\n                title?.let {\n                    CompositionLocalProvider(\n                        LocalContentColor provides titleContentColor,\n                        LocalTextStyle provides MaterialTheme.typography.headlineSmall\n                    ) {\n                        Box(\n                            modifier = Modifier\n                                .padding(horizontalDialogPadding)\n                                .padding(TitlePadding)\n                                .align(\n                                    if (icon == null) Alignment.Start\n                                    else Alignment.CenterHorizontally\n                                )\n                        ) {\n                            title()\n                        }\n                    }\n                }\n                // Top content scroll divider\n                if (contentScrollState.canScrollBackward) {\n                    HorizontalDivider()\n                }\n                // Content\n                CompositionLocalProvider(\n                    LocalContentColor provides textContentColor,\n                    LocalTextStyle provides MaterialTheme.typography.bodyMedium\n                ) {\n                    Box(\n                        Modifier\n                            .weight(weight = 1f, fill = false)\n                            .verticalScroll(contentScrollState)\n                            .align(Alignment.Start)\n                    ) {\n                        content(horizontalDialogPadding)\n                    }\n                }\n                // Bottom content scroll divider\n                if (contentScrollState.canScrollForward) {\n                    HorizontalDivider()\n                }\n                // Buttons\n                Box(\n                    modifier = Modifier\n                        .padding(horizontalDialogPadding)\n                        .align(Alignment.End)\n                ) {\n                    CompositionLocalProvider(\n                        LocalContentColor provides MaterialTheme.colorScheme.primary,\n                        LocalTextStyle provides MaterialTheme.typography.labelLarge,\n                    ) {\n                        FlowRow(\n                            horizontalArrangement = Arrangement.End\n                        ) {\n                            dismissButton?.invoke()\n                            confirmButton()\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nprivate val DialogPadding = PaddingValues(all = 24.dp)\nprivate val IconPadding = PaddingValues(bottom = 16.dp)\nprivate val TitlePadding = PaddingValues(bottom = 16.dp)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/components/ImagePresenter.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding.components\n\ninterface ImagePresenter {\n    fun hasNextImage(): Boolean\n    fun getNextImage(): String?\n}\n\nobject EmptyImagePresenter : ImagePresenter {\n    override fun hasNextImage(): Boolean {\n        return false\n    }\n\n    override fun getNextImage(): String? {\n        return null\n    }\n}\n\nclass RandomImagePresenter(private val imageUrls: List<String>) : ImagePresenter {\n    private val lastShownImages = LinkedHashSet<String>()\n\n    override fun hasNextImage(): Boolean {\n        return imageUrls.isNotEmpty()\n    }\n\n    override fun getNextImage(): String? {\n        if (imageUrls.isEmpty()) {\n            return null\n        }\n        if (imageUrls.size == 1) {\n            return imageUrls.first()\n        }\n\n        val imagePool = imageUrls - lastShownImages\n        if (imagePool.isEmpty()) {\n            val lastImageUrl = lastShownImages.last()\n            lastShownImages.clear()\n            lastShownImages.add(lastImageUrl)\n            return getNextImage()\n        }\n\n        val image = imagePool.random()\n        lastShownImages.add(image)\n        return image\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/components/ImageSlideshow.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding.components\n\nimport android.provider.Settings\nimport androidx.compose.animation.Crossfade\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.Ease\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.graphics.TransformOrigin\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi\nimport com.bumptech.glide.integration.compose.GlideImage\nimport io.github.drumber.kitsune.ui.theme.KitsuneTheme\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.delay\nimport kotlin.random.Random\n\nprivate const val ANIMATION_TRANSFORM_DURATION = 20000\n\n@OptIn(ExperimentalGlideComposeApi::class)\n@Composable\nfun ImageSlideshow(\n    modifier: Modifier = Modifier,\n    imagePresenter: ImagePresenter\n) {\n    var currentImage by rememberSaveable { mutableStateOf(imagePresenter.getNextImage()) }\n    var nextImage by rememberSaveable { mutableStateOf(imagePresenter.getNextImage()) }\n\n    val context = LocalContext.current\n\n    val scaleAnimation = remember { Animatable(1.1f) }\n    val xAnimation = remember { Animatable(0f) }\n    val yAnimation = remember { Animatable(0f) }\n\n    var animationKey by remember { mutableIntStateOf(0) }\n    if (isAnimationsEnabled()) {\n        LaunchedEffect(animationKey) {\n            awaitAll(\n                async {\n                    scaleAnimation.animateTo(\n                        targetValue = 1.3f - scaleAnimation.value + 1.1f,\n                        animationSpec = tween(ANIMATION_TRANSFORM_DURATION, easing = Ease)\n                    )\n                },\n                async {\n                    xAnimation.animateTo(\n                        targetValue = (10f - xAnimation.value) * (if (Random.nextInt() % 2 == 0) 1 else -1),\n                        animationSpec = tween(ANIMATION_TRANSFORM_DURATION, easing = LinearEasing)\n                    )\n                },\n                async {\n                    yAnimation.animateTo(\n                        targetValue = (15f - yAnimation.value) * (if (Random.nextInt() % 2 == 0) 1 else -1),\n                        animationSpec = tween(ANIMATION_TRANSFORM_DURATION, easing = LinearEasing)\n                    )\n                }\n            )\n\n            animationKey = (++animationKey) % 2\n        }\n    }\n\n    LaunchedEffect(currentImage) {\n        // preload next image\n        Glide.with(context)\n            .load(nextImage)\n            .preload()\n\n        delay(8000)\n        currentImage = nextImage\n        nextImage = imagePresenter.getNextImage()\n    }\n\n    Crossfade(\n        targetState = currentImage,\n        label = \"slide show\",\n        modifier = modifier.clipToBounds(),\n        animationSpec = tween(3000)\n    ) { image ->\n        if (image != null) {\n            GlideImage(\n                model = image,\n                contentDescription = null,\n                contentScale = ContentScale.Crop,\n                modifier = Modifier\n                    .fillMaxSize()\n                    .graphicsLayer {\n                        scaleX = scaleAnimation.value\n                        scaleY = scaleX\n                        translationX = xAnimation.value\n                        translationY = yAnimation.value\n                        transformOrigin = TransformOrigin.Center\n                    }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun isAnimationsEnabled(): Boolean {\n    val context = LocalContext.current\n    return remember {\n        val windowAnimationScale = Settings.Global.getFloat(\n            context.contentResolver,\n            Settings.Global.WINDOW_ANIMATION_SCALE,\n            1f\n        )\n        val transitionAnimationScale = Settings.Global.getFloat(\n            context.contentResolver,\n            Settings.Global.TRANSITION_ANIMATION_SCALE,\n            1f\n        )\n        val animatorDurationScale = Settings.Global.getFloat(\n            context.contentResolver,\n            Settings.Global.ANIMATOR_DURATION_SCALE,\n            1f\n        )\n\n        windowAnimationScale != 0f && transitionAnimationScale != 0f && animatorDurationScale != 0f\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun ImageSlideshowPreview() {\n    KitsuneTheme {\n        ImageSlideshow(imagePresenter = EmptyImagePresenter)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/components/OnboardingNavigationControls.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.automirrored.filled.ArrowForward\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.ui.theme.KitsuneTheme\n\n@Composable\nfun OnboardingNavigationControls(\n    modifier: Modifier = Modifier,\n    hideNextButton: Boolean = false,\n    onBackClicked: () -> Unit = {},\n    onNextClicked: () -> Unit = {},\n    backText: String = stringResource(R.string.action_back),\n    nextText: String = stringResource(R.string.action_next)\n) {\n    Row(\n        horizontalArrangement = Arrangement.SpaceBetween,\n        modifier = modifier.fillMaxWidth()\n    ) {\n        TextButton(onClick = onBackClicked) {\n            Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)\n            Spacer(Modifier.width(6.dp))\n            Text(backText)\n        }\n        if (!hideNextButton) {\n            TextButton(onClick = onNextClicked) {\n                Text(nextText)\n                Spacer(Modifier.width(6.dp))\n                Icon(Icons.AutoMirrored.Default.ArrowForward, contentDescription = null)\n            }\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nprivate fun OnboardingNavigationControlsPreview() {\n    KitsuneTheme {\n        OnboardingNavigationControls()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/components/PreferenceCard.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun PreferenceCard(\n    title: @Composable () -> Unit,\n    description: @Composable () -> Unit,\n    action: (@Composable () -> Unit)? = null,\n    onClick: () -> Unit\n) {\n    Surface(\n        onClick = onClick,\n        color = MaterialTheme.colorScheme.surfaceContainer,\n        shape = RoundedCornerShape(24.dp),\n        modifier = Modifier\n            .fillMaxWidth()\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp)\n        ) {\n            Column(modifier = Modifier.weight(1f, true)) {\n                CompositionLocalProvider(\n                    LocalTextStyle provides LocalTextStyle.current.merge(MaterialTheme.typography.bodyLarge)\n                ) {\n                    title()\n                }\n                Spacer(Modifier.height(2.dp))\n                CompositionLocalProvider(\n                    LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,\n                    LocalTextStyle provides LocalTextStyle.current.merge(MaterialTheme.typography.bodyMedium)\n                ) {\n                    description()\n                }\n            }\n            if (action != null) {\n                Spacer(Modifier.width(4.dp))\n                action()\n            }\n        }\n    }\n}\n\n@Preview(showBackground = false)\n@Composable\nprivate fun PreferenceCardPreview() {\n    PreferenceCard(\n        title = { Text(\"Check for Updates\") },\n        description = { Text(\"Get notified when a new release is available on GitHub.\") },\n        action = {\n            Switch(\n                checked = false,\n                onCheckedChange = {}\n            )\n        },\n        onClick = {}\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/pages/LoginPage.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding.pages\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.adaptive.currentWindowAdaptiveInfo\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.window.core.layout.WindowSizeClass\nimport androidx.window.core.layout.WindowWidthSizeClass\nimport com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi\nimport com.bumptech.glide.integration.compose.GlideImage\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.ui.onboarding.components.OnboardingNavigationControls\nimport io.github.drumber.kitsune.ui.theme.KitsuneTheme\n\n@Composable\nfun LoginPage(\n    modifier: Modifier = Modifier,\n    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass,\n    localUser: LocalUser? = null,\n    onLoginClicked: () -> Unit = {},\n    onCreateAccountClicked: () -> Unit = {},\n    onBack: () -> Unit = {},\n    onNext: () -> Unit = {}\n) {\n    val backgroundGradient = listOf(\n        MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f),\n        MaterialTheme.colorScheme.surface\n    )\n\n    Column(\n        verticalArrangement = Arrangement.Center,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = Modifier\n            .fillMaxSize()\n            .background(Brush.verticalGradient(backgroundGradient))\n            .then(modifier)\n            .padding(16.dp)\n    ) {\n        if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT) {\n            Column(\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.CenterHorizontally,\n                modifier = Modifier.weight(1f)\n            ) {\n                HeaderSection()\n                Spacer(modifier = Modifier.height(32.dp))\n                if (localUser != null) {\n                    LoggedInUserSection(\n                        modifier = Modifier.fillMaxWidth(),\n                        localUser = localUser,\n                        onNextClicked = onNext\n                    )\n                } else {\n                    ActionSection(\n                        onLoginClicked = onLoginClicked,\n                        onCreateAccountClicked = onCreateAccountClicked\n                    )\n                }\n            }\n        } else {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.SpaceEvenly,\n                modifier = Modifier.weight(1f)\n            ) {\n                HeaderSection(modifier = Modifier.fillMaxWidth(0.5f))\n                Spacer(modifier = Modifier.width(32.dp))\n                Column {\n                    if (localUser != null) {\n                        LoggedInUserSection(\n                            modifier = Modifier.fillMaxWidth(),\n                            localUser = localUser,\n                            onNextClicked = onNext\n                        )\n                    } else {\n                        ActionSection(\n                            onLoginClicked = onLoginClicked,\n                            onCreateAccountClicked = onCreateAccountClicked\n                        )\n                    }\n                }\n            }\n        }\n        Spacer(modifier = Modifier.height(8.dp))\n        OnboardingNavigationControls(\n            hideNextButton = localUser != null,\n            onBackClicked = onBack,\n            onNextClicked = onNext,\n            nextText = stringResource(R.string.action_skip)\n        )\n    }\n}\n\n@Composable\nprivate fun HeaderSection(modifier: Modifier = Modifier) {\n    Column(\n        verticalArrangement = Arrangement.Center,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = modifier\n    ) {\n        Row(\n            modifier = Modifier.weight(1f, false),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.Center\n        ) {\n            Image(\n                painter = painterResource(R.drawable.onboarding_login_logo),\n                contentDescription = null,\n                modifier = Modifier\n                    .size(148.dp)\n            )\n        }\n        Spacer(modifier = Modifier.height(16.dp))\n        Text(\n            text = stringResource(R.string.onboarding_login_title),\n            style = MaterialTheme.typography.headlineLarge,\n            color = MaterialTheme.colorScheme.primary,\n            textAlign = TextAlign.Center\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n        Text(\n            text = stringResource(R.string.onboarding_login_subtitle),\n            style = MaterialTheme.typography.bodyLarge,\n            textAlign = TextAlign.Center\n        )\n    }\n}\n\n@OptIn(ExperimentalGlideComposeApi::class)\n@Composable\nprivate fun LoggedInUserSection(\n    modifier: Modifier = Modifier,\n    localUser: LocalUser,\n    onNextClicked: () -> Unit = {}\n) {\n    Column(modifier = modifier) {\n        Card(\n            shape = CircleShape,\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier.padding(12.dp)\n            ) {\n                GlideImage(\n                    model = localUser.avatar?.originalOrDown(),\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier = Modifier\n                        .size(48.dp)\n                        .clip(CircleShape)\n                        .background(Color.Gray)\n                ) {\n                    it.placeholder(R.drawable.profile_picture_placeholder)\n                }\n                Spacer(Modifier.width(16.dp))\n                Column {\n                    Text(\n                        style = MaterialTheme.typography.titleMedium,\n                        text = localUser.name ?: stringResource(R.string.no_information)\n                    )\n                    Spacer(Modifier.height(2.dp))\n                    Text(\n                        style = MaterialTheme.typography.bodySmall,\n                        text = stringResource(R.string.onboarding_login_is_logged_in)\n                    )\n                }\n            }\n        }\n        Spacer(modifier = Modifier.height(8.dp))\n        Button(\n            onClick = onNextClicked,\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            Text(text = stringResource(R.string.action_continue))\n        }\n    }\n}\n\n@Composable\nprivate fun ActionSection(\n    modifier: Modifier = Modifier,\n    isDisabled: Boolean = false,\n    onLoginClicked: () -> Unit = {},\n    onCreateAccountClicked: () -> Unit = {}\n) {\n    Column(\n        verticalArrangement = Arrangement.Center,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = modifier\n    ) {\n        Button(\n            onClick = onLoginClicked,\n            enabled = !isDisabled,\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            Text(text = stringResource(R.string.action_log_in))\n        }\n        Spacer(modifier = Modifier.height(8.dp))\n        OutlinedButton(\n            onClick = onCreateAccountClicked,\n            enabled = !isDisabled,\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            Text(text = stringResource(R.string.action_create_account))\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nprivate fun LoginPagePreview() {\n    KitsuneTheme {\n        LoginPage(localUser = null)\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nprivate fun LoginPageAuthenticatedPreview() {\n    KitsuneTheme {\n        LoginPage(localUser = LocalUser.empty(\"\"))\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/pages/SetupPage.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding.pages\n\nimport android.Manifest\nimport android.os.Build\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.selection.selectableGroup\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Warning\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.asFlow\nimport com.chibatching.kotpref.livedata.asLiveData\nimport com.google.accompanist.permissions.ExperimentalPermissionsApi\nimport com.google.accompanist.permissions.isGranted\nimport com.google.accompanist.permissions.rememberPermissionState\nimport com.google.accompanist.permissions.shouldShowRationale\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalTitleLanguagePreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.onboarding.components.CustomDialog\nimport io.github.drumber.kitsune.ui.onboarding.components.OnboardingNavigationControls\nimport io.github.drumber.kitsune.ui.onboarding.components.PreferenceCard\nimport io.github.drumber.kitsune.ui.theme.KitsuneTheme\nimport kotlinx.coroutines.flow.map\n\n@OptIn(ExperimentalPermissionsApi::class)\n@Composable\nfun SetupPageAdapter(\n    modifier: Modifier = Modifier,\n    localUser: LocalUser? = null,\n    onFinishClicked: () -> Unit = {},\n    onBack: () -> Unit = {}\n) {\n    val notificationPermissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n        rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)\n    } else {\n        null\n    }\n    val isNotificationPermissionGranted = notificationPermissionState?.status?.isGranted ?: true\n    val shouldShouldNotificationPermissionNotice = notificationPermissionState?.status?.shouldShowRationale ?: false\n\n    val checkForUpdatesPreference by KitsunePref\n        .asLiveData(KitsunePref::checkForUpdatesOnStart)\n        .asFlow()\n        .collectAsState(false)\n\n    val updateCheckForUpdatesPreference: (Boolean) -> Unit = { value: Boolean ->\n        if (isNotificationPermissionGranted) {\n            KitsunePref.checkForUpdatesOnStart = value\n        } else {\n            notificationPermissionState?.launchPermissionRequest()\n        }\n    }\n\n    val titleLanguages = LocalTitleLanguagePreference.entries.map { it.name }\n    val selectedTitleLanguageIndex by KitsunePref.getTitleLanguageAsFlow()\n        .map { it.ordinal }\n        .collectAsState(KitsunePref.titles.ordinal)\n    val selectTitleLanguage = { index: Int ->\n        KitsunePref.titles = LocalTitleLanguagePreference.entries[index]\n    }\n\n    SetupPage(\n        modifier = modifier,\n        onFinishClicked = onFinishClicked,\n        onBackClicked = onBack,\n        showNotificationPermissionNotice = shouldShouldNotificationPermissionNotice,\n        checkForUpdatesPreference = checkForUpdatesPreference,\n        onCheckForUpdatesPreferenceChanged = updateCheckForUpdatesPreference,\n        hideTitleLanguagePreference = localUser != null,\n        titleLanguages = titleLanguages,\n        selectedTitleLanguageIndex = selectedTitleLanguageIndex,\n        onTitleLanguageSelected = selectTitleLanguage\n    )\n}\n\n@Composable\nprivate fun SetupPage(\n    modifier: Modifier = Modifier,\n    onFinishClicked: () -> Unit = {},\n    onBackClicked: () -> Unit = {},\n    showNotificationPermissionNotice: Boolean = false,\n    checkForUpdatesPreference: Boolean = false,\n    onCheckForUpdatesPreferenceChanged: (Boolean) -> Unit = {},\n    hideTitleLanguagePreference: Boolean = false,\n    titleLanguages: List<String> = emptyList(),\n    selectedTitleLanguageIndex: Int = 0,\n    onTitleLanguageSelected: (Int) -> Unit = {}\n) {\n    val backgroundGradient = listOf(\n        MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f),\n        MaterialTheme.colorScheme.surface\n    )\n\n    var openSelectTitleLanguageDialog by rememberSaveable { mutableStateOf(false) }\n\n    if (openSelectTitleLanguageDialog) {\n        SelectTitleLanguageDialog(\n            titleLanguages = titleLanguages,\n            selectedIndex = selectedTitleLanguageIndex,\n            onTitleLanguageSelected = {\n                onTitleLanguageSelected(it)\n                openSelectTitleLanguageDialog = false\n            },\n            onDismiss = { openSelectTitleLanguageDialog = false }\n        )\n    }\n\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n        modifier = Modifier\n            .fillMaxSize()\n            .background(Brush.verticalGradient(backgroundGradient))\n            .then(modifier)\n    ) {\n        Column(\n            modifier = Modifier\n                .weight(1f)\n                .verticalScroll(state = rememberScrollState())\n                .widthIn(min = 0.dp, max = 500.dp)\n                .padding(16.dp)\n        ) {\n            Spacer(Modifier.weight(1f))\n            HeaderSection()\n            Spacer(modifier = Modifier.height(32.dp))\n            PreferenceCard(\n                title = { Text(stringResource(R.string.onboarding_setup_updates)) },\n                description = {\n                    Text(stringResource(R.string.onboarding_setup_updates_description))\n                    if (showNotificationPermissionNotice) {\n                        Spacer(Modifier.height(8.dp))\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Icon(\n                                imageVector = Icons.Outlined.Warning,\n                                contentDescription = null,\n                                tint = MaterialTheme.colorScheme.error\n                            )\n                            Spacer(Modifier.width(8.dp))\n                            Text(\n                                text = stringResource(R.string.onboarding_setup_notification_permission_notice),\n                                color = MaterialTheme.colorScheme.error\n                            )\n                        }\n                    }\n                },\n                action = {\n                    Switch(\n                        checked = checkForUpdatesPreference,\n                        onCheckedChange = onCheckForUpdatesPreferenceChanged\n                    )\n                },\n                onClick = {\n                    onCheckForUpdatesPreferenceChanged(!checkForUpdatesPreference)\n                }\n            )\n            if (!hideTitleLanguagePreference) {\n                Spacer(Modifier.height(12.dp))\n                PreferenceCard(\n                    title = { Text(stringResource(R.string.onboarding_setup_title_language)) },\n                    description = {\n                        Text(stringResource(R.string.onboarding_setup_title_language_description))\n                        if (selectedTitleLanguageIndex in titleLanguages.indices) {\n                            Text(\n                                stringResource(\n                                    R.string.onboarding_setup_title_language_selected,\n                                    titleLanguages[selectedTitleLanguageIndex]\n                                )\n                            )\n                        }\n                    },\n                    onClick = { openSelectTitleLanguageDialog = true }\n                )\n            }\n            Spacer(Modifier.weight(1f))\n            Spacer(Modifier.height(32.dp))\n            Button(\n                onClick = onFinishClicked,\n                modifier = Modifier.fillMaxWidth()\n            ) {\n                Text(text = stringResource(R.string.onboarding_setup_action))\n            }\n            Spacer(modifier = Modifier.height(8.dp))\n        }\n        OnboardingNavigationControls(\n            hideNextButton = true,\n            onBackClicked = onBackClicked,\n            modifier = Modifier\n                .padding(horizontal = 16.dp)\n                .padding(bottom = 16.dp)\n        )\n    }\n}\n\n@Composable\nprivate fun HeaderSection(modifier: Modifier = Modifier) {\n    Column(\n        verticalArrangement = Arrangement.Center,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = modifier\n    ) {\n        Text(\n            text = stringResource(R.string.onboarding_setup_title),\n            style = MaterialTheme.typography.headlineLarge,\n            color = MaterialTheme.colorScheme.primary,\n            textAlign = TextAlign.Center\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n        Text(\n            text = stringResource(R.string.onboarding_setup_subtitle),\n            style = MaterialTheme.typography.bodyLarge,\n            textAlign = TextAlign.Center\n        )\n    }\n}\n\n@Composable\nprivate fun SelectTitleLanguageDialog(\n    titleLanguages: List<String>,\n    selectedIndex: Int,\n    onTitleLanguageSelected: (Int) -> Unit,\n    onDismiss: () -> Unit\n) {\n    var tmpSelectedOption by remember { mutableIntStateOf(selectedIndex) }\n\n    CustomDialog(\n        title = { Text(stringResource(R.string.onboarding_setup_title_language)) },\n        onDismissRequest = onDismiss,\n        confirmButton = {\n            TextButton(onClick = { onTitleLanguageSelected(tmpSelectedOption) }) {\n                Text(stringResource(R.string.action_ok))\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismiss) {\n                Text(stringResource(R.string.action_cancel))\n            }\n        }\n    ) { contentPadding ->\n        Column(modifier = Modifier.selectableGroup()) {\n            titleLanguages.forEachIndexed { index, language ->\n                Row(\n                    Modifier\n                        .fillMaxWidth()\n                        .height(56.dp)\n                        .selectable(\n                            selected = index == tmpSelectedOption,\n                            onClick = { tmpSelectedOption = index },\n                            role = Role.RadioButton\n                        )\n                        .padding(contentPadding),\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    RadioButton(\n                        selected = index == tmpSelectedOption,\n                        onClick = null\n                    )\n                    Text(\n                        text = language,\n                        style = MaterialTheme.typography.bodyLarge,\n                        modifier = Modifier.padding(start = 16.dp)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nprivate fun SetupPagePreview() {\n    KitsuneTheme {\n        SetupPage()\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nprivate fun SelectTitleLanguageDialogPreview() {\n    KitsuneTheme {\n        SelectTitleLanguageDialog(\n            titleLanguages = listOf(\"Canonical\", \"Romaji\", \"English\"),\n            selectedIndex = 2,\n            onTitleLanguageSelected = {},\n            onDismiss = {}\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/onboarding/pages/WelcomePage.kt",
    "content": "package io.github.drumber.kitsune.ui.onboarding.pages\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.sizeIn\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.adaptive.currentWindowAdaptiveInfo\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewScreenSizes\nimport androidx.compose.ui.unit.dp\nimport androidx.window.core.layout.WindowHeightSizeClass\nimport androidx.window.core.layout.WindowSizeClass\nimport androidx.window.core.layout.WindowWidthSizeClass\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.ui.onboarding.OnboardingUiState\nimport io.github.drumber.kitsune.ui.onboarding.components.ImageSlideshow\nimport io.github.drumber.kitsune.ui.onboarding.components.RandomImagePresenter\nimport io.github.drumber.kitsune.ui.theme.KitsuneTheme\n\n@Composable\nfun WelcomePage(\n    modifier: Modifier = Modifier,\n    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass,\n    uiState: OnboardingUiState,\n    onNextClicked: () -> Unit = {}\n) {\n    val overlayGradient = listOf(\n        MaterialTheme.colorScheme.surfaceDim.copy(alpha = 0.75f), MaterialTheme.colorScheme.surface\n    )\n\n    val imagePresenter = remember(uiState.backgroundImages) {\n        RandomImagePresenter(uiState.backgroundImages.shuffled())\n    }\n\n    val isVerticalLayout = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedVisibility(\n            visible = imagePresenter.hasNextImage(),\n            enter = fadeIn(tween(durationMillis = 800)),\n            exit = fadeOut()\n        ) {\n            ImageSlideshow(modifier = Modifier.fillMaxSize(), imagePresenter = imagePresenter)\n        }\n        Column(\n            verticalArrangement = if (isVerticalLayout) Arrangement.Bottom else Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally,\n            modifier = Modifier\n                .fillMaxSize()\n                .background(Brush.verticalGradient(overlayGradient))\n                .verticalScroll(state = rememberScrollState())\n                .then(modifier)\n                .padding(16.dp)\n        ) {\n            if (isVerticalLayout) {\n                HeaderSection(isCompact = windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT)\n                Spacer(modifier = Modifier.height(32.dp))\n                FeatureSection(modifier = Modifier.fillMaxWidth())\n                Spacer(modifier = Modifier.height(32.dp))\n                GetStartedButton(modifier = Modifier.fillMaxWidth(), onClick = onNextClicked)\n                Spacer(modifier = Modifier.height(32.dp))\n            } else {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceEvenly,\n                    modifier = Modifier.fillMaxWidth()\n                ) {\n                    HeaderSection(\n                        isCompact = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.EXPANDED,\n                        modifier = Modifier\n                            .fillMaxWidth(0.5f)\n                            .fillMaxHeight()\n                    )\n                    Spacer(modifier = Modifier.width(32.dp))\n                    Column(modifier = Modifier.width(IntrinsicSize.Min)) {\n                        FeatureSection(modifier = Modifier.width(IntrinsicSize.Max))\n                        Spacer(modifier = Modifier.height(32.dp))\n                        GetStartedButton(\n                            modifier = Modifier.fillMaxWidth(), onClick = onNextClicked\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun HeaderSection(modifier: Modifier = Modifier, isCompact: Boolean = false) {\n    val maxLogoHeight = if (isCompact) 100.dp else 180.dp\n\n    Column(\n        verticalArrangement = Arrangement.Center,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = modifier.wrapContentHeight()\n    ) {\n        Image(\n            painter = painterResource(id = R.drawable.ic_launcher_foreground),\n            contentDescription = null,\n            modifier = Modifier\n                .sizeIn(\n                    maxWidth = maxLogoHeight,\n                    maxHeight = maxLogoHeight,\n                    minWidth = 50.dp,\n                    minHeight = 50.dp\n                )\n                .aspectRatio(1f)\n        )\n        Spacer(\n            modifier = if (isCompact) {\n                Modifier.height(0.dp)\n            } else {\n                Modifier.height(2.dp)\n            }\n        )\n        Text(\n            text = stringResource(R.string.onboarding_welcome_title),\n            style = MaterialTheme.typography.headlineLarge,\n            color = MaterialTheme.colorScheme.primary,\n            textAlign = TextAlign.Center\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n        Text(\n            text = stringResource(R.string.onboarding_welcome_subtitle),\n            style = MaterialTheme.typography.bodyLarge,\n            color = MaterialTheme.colorScheme.onSurface,\n            textAlign = TextAlign.Center\n        )\n    }\n}\n\n@Composable\nprivate fun FeatureSection(modifier: Modifier = Modifier) {\n    Column(modifier = modifier) {\n        FeatureItem(\n            icon = painterResource(R.drawable.ic_search_24),\n            title = stringResource(R.string.onboarding_welcome_feature_search),\n            description = stringResource(R.string.onboarding_welcome_feature_search_description)\n        )\n        FeatureItem(\n            icon = painterResource(R.drawable.ic_outline_explore_24),\n            title = stringResource(R.string.onboarding_welcome_feature_explore),\n            description = stringResource(R.string.onboarding_welcome_feature_explore_description)\n        )\n        FeatureItem(\n            icon = painterResource(R.drawable.ic_outline_bookmarks_24),\n            title = stringResource(R.string.onboarding_welcome_feature_track),\n            description = stringResource(R.string.onboarding_welcome_feature_track_description)\n        )\n    }\n}\n\n@Composable\nprivate fun FeatureItem(icon: Painter, title: String, description: String) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)\n    ) {\n        Icon(\n            painter = icon,\n            contentDescription = null,\n            tint = MaterialTheme.colorScheme.primary,\n            modifier = Modifier.size(40.dp)\n        )\n        Spacer(modifier = Modifier.width(16.dp))\n        Column {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.onSurface\n            )\n            Spacer(modifier = Modifier.height(2.dp))\n            Text(\n                text = description,\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun GetStartedButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {\n    Button(\n        modifier = modifier, onClick = onClick\n    ) {\n        Text(text = stringResource(R.string.onboarding_welcome_action))\n    }\n}\n\n@PreviewScreenSizes\n@Preview(showBackground = true, heightDp = 780, widthDp = 390)\n@Composable\nprivate fun WelcomePagePreview() {\n    KitsuneTheme {\n        WelcomePage(uiState = OnboardingUiState())\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/permissions/NotificationPermission.kt",
    "content": "package io.github.drumber.kitsune.ui.permissions\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.app.ActivityCompat\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport io.github.drumber.kitsune.R\n\nfun Context.isNotificationPermissionGranted(): Boolean {\n    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n        ActivityCompat.checkSelfPermission(\n            this,\n            Manifest.permission.POST_NOTIFICATIONS\n        ) == PackageManager.PERMISSION_GRANTED\n    } else {\n        // no need to request permission on android versions below 33\n        true\n    }\n}\n\n@SuppressLint(\"InlinedApi\")\nfun Activity.requestNotificationPermission(\n    requestPermissionLauncher: ActivityResultLauncher<String>,\n    onRationaleDialogDismiss: (() -> Unit)? = null\n) {\n    if (isNotificationPermissionGranted()) return\n\n    if (\n        ActivityCompat.shouldShowRequestPermissionRationale(\n            this,\n            Manifest.permission.POST_NOTIFICATIONS\n        )\n    ) {\n        MaterialAlertDialogBuilder(this)\n            .setTitle(R.string.dialog_request_notification_permission_title)\n            .setMessage(R.string.dialog_request_notification_permission)\n            .setNegativeButton(R.string.action_cancel) { dialog, _ ->\n                dialog.dismiss()\n                onRationaleDialogDismiss?.invoke()\n            }\n            .setPositiveButton(R.string.action_allow) { dialog, _ ->\n                dialog.dismiss()\n                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)\n            }\n            .show()\n    } else {\n        requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)\n    }\n}\n\nfun Context.showNotificationPermissionRejectedDialog(): AlertDialog = MaterialAlertDialogBuilder(this)\n    .setTitle(R.string.dialog_notification_permission_rejected_title)\n    .setMessage(R.string.dialog_notification_permission_rejected)\n    .setPositiveButton(R.string.action_ok) { dialog, _ ->\n        dialog.dismiss()\n    }\n    .show()\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/photoview/PhotoViewActivity.kt",
    "content": "package io.github.drumber.kitsune.ui.photoview\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.graphics.Color\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.Looper\nimport android.view.View\nimport android.widget.Toast\nimport androidx.core.app.ActivityCompat\nimport androidx.core.content.ContextCompat\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.core.view.WindowInsetsControllerCompat\nimport androidx.core.view.isVisible\nimport androidx.lifecycle.lifecycleScope\nimport androidx.navigation.navArgs\nimport app.futured.hauler.setOnDragActivityListener\nimport app.futured.hauler.setOnDragDismissedListener\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.load.DataSource\nimport com.bumptech.glide.load.engine.GlideException\nimport com.bumptech.glide.request.RequestListener\nimport com.bumptech.glide.request.target.Target\nimport com.google.android.material.transition.platform.MaterialContainerTransform\nimport com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.ActivityPhotoViewBinding\nimport io.github.drumber.kitsune.ui.base.BaseActivity\nimport io.github.drumber.kitsune.util.extensions.clearLightNavigationBar\nimport io.github.drumber.kitsune.util.extensions.clearLightStatusBar\nimport io.github.drumber.kitsune.util.extensions.isNightMode\nimport io.github.drumber.kitsune.util.extensions.showSomethingWrongToast\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.saveImageInGallery\nimport io.github.drumber.kitsune.util.ui.initHeightWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initMarginWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.IOException\n\nclass PhotoViewActivity : BaseActivity(setAppTheme = false) {\n\n    private lateinit var binding: ActivityPhotoViewBinding\n\n    private val args by navArgs<PhotoViewActivityArgs>()\n\n    private var isFullscreen: Boolean = false\n\n    private val hideHandler = Handler(Looper.getMainLooper())\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        ViewCompat.setTransitionName(findViewById(android.R.id.content), args.transitionName)\n        setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())\n        window.sharedElementEnterTransition = MaterialContainerTransform().apply {\n            addTarget(android.R.id.content)\n            duration = resources.getInteger(R.integer.material_motion_duration_medium_2).toLong()\n        }\n        window.sharedElementReturnTransition = MaterialContainerTransform().apply {\n            addTarget(android.R.id.content)\n            duration = resources.getInteger(R.integer.material_motion_duration_medium_1).toLong()\n        }\n\n        binding = ActivityPhotoViewBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        if (!isNightMode()) {\n            clearLightStatusBar()\n            clearLightNavigationBar()\n        }\n\n        WindowInsetsControllerCompat(window, binding.root).systemBarsBehavior =\n            WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE\n\n        binding.photoView.apply {\n            setOnClickListener { toggleSystemUi() }\n            setAllowParentInterceptOnEdge(false) // we manage scroll interception on our own\n            setOnMatrixChangeListener {\n                // disallow touch event interception unless image is scrolled to the top\n                binding.nestedScrollView.requestDisallowInterceptTouchEvent(it.top < 0f)\n            }\n        }\n\n        Glide.with(this)\n            .load(args.imageUrl)\n            .thumbnail(\n                Glide.with(this)\n                    .load(args.thumbnailUrl)\n                    .dontTransform()\n            )\n            .listener(object : RequestListener<Drawable> {\n                override fun onLoadFailed(\n                    e: GlideException?,\n                    model: Any?,\n                    target: Target<Drawable>,\n                    isFirstResource: Boolean\n                ): Boolean {\n                    binding.progressIndicator.hide()\n                    val shouldHaveThumbnailLoaded =\n                        !isFirstResource && !args.thumbnailUrl.isNullOrBlank()\n                    onImageLoadFailed(!shouldHaveThumbnailLoaded)\n                    return false\n                }\n\n                override fun onResourceReady(\n                    resource: Drawable,\n                    model: Any,\n                    target: Target<Drawable>?,\n                    dataSource: DataSource,\n                    isFirstResource: Boolean\n                ): Boolean {\n                    binding.progressIndicator.hide()\n                    return false\n                }\n            })\n            .dontTransform()\n            .into(binding.photoView)\n\n        binding.apply {\n            btnClose.resetAutoHideOnTouch()\n            btnSave.resetAutoHideOnTouch()\n            btnOpen.resetAutoHideOnTouch()\n\n            btnClose.setOnClickListener { finish() }\n            btnSave.setOnClickListener { saveImage() }\n            btnOpen.setOnClickListener {\n                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(args.imageUrl))\n                startActivity(intent)\n            }\n\n            statusBarBackground.initHeightWindowInsetsListener(consume = false)\n            progressIndicator.initMarginWindowInsetsListener(top = true, consume = false)\n            fullscreenContentControls.initPaddingWindowInsetsListener(\n                left = true,\n                right = true,\n                bottom = true,\n                consume = false\n            )\n        }\n\n        binding.haulerView.setOnDragDismissedListener { finish() }\n        binding.haulerView.setOnDragActivityListener { _, rawOffset ->\n            // fade background alpha on drag\n            val alpha = (1.0f - rawOffset) * 255.0f\n            val color = Color.argb(alpha.toInt(), 0, 0, 0)\n            binding.root.setBackgroundColor(color)\n        }\n        // reset background color\n        binding.root.setBackgroundColor(Color.BLACK)\n    }\n\n    private fun onImageLoadFailed(exit: Boolean) {\n        Toast.makeText(this, R.string.error_image_loading, Toast.LENGTH_SHORT).show()\n        if (exit) {\n            finish()\n        }\n    }\n\n    override fun onPostCreate(savedInstanceState: Bundle?) {\n        super.onPostCreate(savedInstanceState)\n\n        hideHandler.removeCallbacks(hideRunnable)\n        hideHandler.postDelayed(hideRunnable, UI_ANIMATION_DELAY)\n    }\n\n    private val hideRunnable = Runnable {\n        hideSystemUi()\n    }\n\n    /**\n     * Resets any hide callback to avoid hiding controls while the user is interacting.\n     */\n    private fun resetAutoHideTime() {\n        if (!isFullscreen) {\n            hideHandler.removeCallbacks(hideRunnable)\n            hideHandler.postDelayed(hideRunnable, AUTO_HIDE_DELAY_MILLIS)\n        }\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private fun View.resetAutoHideOnTouch() {\n        setOnTouchListener { _, _ ->\n            resetAutoHideTime()\n            false\n        }\n    }\n\n    private fun hideSystemUi() {\n        isFullscreen = true\n        WindowInsetsControllerCompat(window, binding.root)\n            .hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())\n        binding.fullscreenContentControls.isVisible = false\n        binding.statusBarBackground.isVisible = false\n    }\n\n    private fun showSystemUi() {\n        isFullscreen = false\n        WindowInsetsControllerCompat(window, binding.root)\n            .show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())\n        binding.fullscreenContentControls.isVisible = true\n        binding.statusBarBackground.isVisible = true\n    }\n\n    private fun toggleSystemUi() {\n        if (isFullscreen) {\n            hideHandler.removeCallbacks(hideRunnable)\n            if (AUTO_HIDE) {\n                hideHandler.postDelayed(hideRunnable, AUTO_HIDE_DELAY_MILLIS)\n            }\n            showSystemUi()\n        } else {\n            hideSystemUi()\n        }\n    }\n\n    override fun onRequestPermissionsResult(\n        requestCode: Int,\n        permissions: Array<out String>,\n        grantResults: IntArray\n    ) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n        if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST_CODE\n            && grantResults.isNotEmpty()\n            && grantResults[0] == PackageManager.PERMISSION_GRANTED\n        ) {\n            saveImage()\n        } else if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST_CODE) {\n            Toast.makeText(\n                this,\n                R.string.error_requires_external_storage_permission,\n                Toast.LENGTH_LONG\n            ).show()\n        }\n    }\n\n    private fun saveImage() {\n        // check if permission is granted\n        if (shouldRequestWritePermission()) {\n            ActivityCompat.requestPermissions(\n                this,\n                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),\n                WRITE_EXTERNAL_STORAGE_REQUEST_CODE\n            )\n            return\n        }\n\n        lifecycleScope.launch(Dispatchers.IO) {\n            val bitmap = try {\n                Glide.with(this@PhotoViewActivity)\n                    .asBitmap()\n                    .load(args.imageUrl)\n                    .submit()\n                    .get()\n            } catch (e: Exception) {\n                runOnUiThread { onImageLoadFailed(false) }\n                return@launch\n            }\n\n            val success = try {\n                saveImageInGallery(bitmap, args.title)\n            } catch (e: IOException) {\n                logE(\"Failed to save image in gallery.\", e)\n                false\n            }\n            withContext(Dispatchers.Main) {\n                if (success) {\n                    Toast.makeText(\n                        this@PhotoViewActivity,\n                        R.string.info_image_saved_in_gallery,\n                        Toast.LENGTH_SHORT\n                    ).show()\n                } else {\n                    showSomethingWrongToast()\n                }\n            }\n        }\n    }\n\n    /**\n     * Check if external storage write permission must be requested.\n     * On Android 10+ this is no longer required, see\n     * [here](https://developer.android.com/training/data-storage/shared/media#scoped_storage_enabled).\n     */\n    private fun shouldRequestWritePermission(): Boolean {\n        return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&\n                ContextCompat.checkSelfPermission(\n                    this,\n                    Manifest.permission.WRITE_EXTERNAL_STORAGE\n                ) != PackageManager.PERMISSION_GRANTED\n    }\n\n    companion object {\n        /**\n         * Whether or not the system UI should be auto-hidden after\n         * [AUTO_HIDE_DELAY_MILLIS] milliseconds.\n         */\n        private const val AUTO_HIDE = true\n\n        /**\n         * If [AUTO_HIDE] is set, the number of milliseconds after\n         * hiding the system UI.\n         */\n        private const val AUTO_HIDE_DELAY_MILLIS = 5000L\n\n        /**\n         * Hide system UI after this amount of milliseconds.\n         */\n        private const val UI_ANIMATION_DELAY = 500L\n\n        private const val WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 1\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/profile/ProfileFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.profile\n\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.graphics.drawable.BitmapDrawable\nimport android.graphics.drawable.ColorDrawable\nimport android.graphics.drawable.Drawable\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.View.OnClickListener\nimport android.view.ViewGroup\nimport android.webkit.URLUtil\nimport android.widget.ImageView\nimport androidx.annotation.StringRes\nimport androidx.core.view.children\nimport androidx.core.view.doOnPreDraw\nimport androidx.core.view.isVisible\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport androidx.navigation.fragment.FragmentNavigatorExtras\nimport androidx.navigation.fragment.findNavController\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.request.target.CustomTarget\nimport com.bumptech.glide.request.transition.Transition\nimport com.github.mikephil.charting.data.PieDataSet\nimport com.github.mikephil.charting.data.PieEntry\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.elevation.SurfaceColors\nimport com.google.android.material.navigation.NavigationBarView\nimport com.google.android.material.tabs.TabLayoutMediator\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.constants.MediaItemSize\nimport io.github.drumber.kitsune.data.presentation.dto.toCharacterDto\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaDto\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.presentation.model.user.Favorite\nimport io.github.drumber.kitsune.data.presentation.model.user.User\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLink\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStats\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStatsData\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStatsKind\nimport io.github.drumber.kitsune.databinding.FragmentProfileBinding\nimport io.github.drumber.kitsune.databinding.ItemProfileSiteChipBinding\nimport io.github.drumber.kitsune.ui.adapter.CharacterAdapter\nimport io.github.drumber.kitsune.ui.adapter.MediaRecyclerViewAdapter\nimport io.github.drumber.kitsune.ui.authentication.AuthenticationActivity\nimport io.github.drumber.kitsune.ui.base.BaseFragment\nimport io.github.drumber.kitsune.ui.component.chart.PieChartStyle\nimport io.github.drumber.kitsune.ui.webview.WebViewFragmentDirections\nimport io.github.drumber.kitsune.util.extensions.copyToClipboard\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.extensions.openPhotoViewActivity\nimport io.github.drumber.kitsune.util.extensions.openUrl\nimport io.github.drumber.kitsune.util.extensions.recyclerView\nimport io.github.drumber.kitsune.util.extensions.setAppTheme\nimport io.github.drumber.kitsune.util.extensions.showSomethingWrongToast\nimport io.github.drumber.kitsune.util.extensions.startUrlShareIntent\nimport io.github.drumber.kitsune.util.extensions.toPx\nimport io.github.drumber.kitsune.util.ui.getProfileSiteLogoResourceId\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\nimport java.util.concurrent.CopyOnWriteArrayList\nimport kotlin.math.round\n\nclass ProfileFragment : BaseFragment(R.layout.fragment_profile, true),\n    NavigationBarView.OnItemReselectedListener {\n\n    private var _binding: FragmentProfileBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: ProfileViewModel by viewModel()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentProfileBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        postponeEnterTransition()\n        view.doOnPreDraw { startPostponedEnterTransition() }\n\n        initToolbar()\n        updateOptionsMenu()\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.userModel.collectLatest { user ->\n                updateUser(user)\n                updateProfileLinks(user?.profileLinks ?: emptyList())\n                updateOptionsMenu()\n                binding.swipeRefreshLayout.isEnabled = user != null\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.uiState.collectLatest { state ->\n                binding.swipeRefreshLayout.apply {\n                    isRefreshing = isRefreshing && state.isRefreshing\n                }\n            }\n        }\n\n        binding.apply {\n            btnLogin.setOnClickListener {\n                val intent = Intent(requireActivity(), AuthenticationActivity::class.java)\n                startActivity(intent)\n            }\n\n            swipeRefreshLayout.initPaddingWindowInsetsListener(\n                left = true,\n                right = true,\n                consume = false\n            )\n            nsvContent.initPaddingWindowInsetsListener(bottom = true, consume = false)\n\n            swipeRefreshLayout.apply {\n                setAppTheme()\n                setOnRefreshListener {\n                    viewModel.refreshUser()\n                }\n            }\n\n            ivCover.setOnClickListener {\n                val coverImgUrl = viewModel.getUser()?.coverImage?.originalOrDown()\n                    ?: return@setOnClickListener\n                val title = viewModel.getUser()?.name?.let { \"$it Cover\" }\n                openPhotoViewActivity(coverImgUrl, title, null, ivCover)\n            }\n\n            val onWaifuClicked: OnClickListener = object : OnClickListener {\n                override fun onClick(v: View?) {\n                    val waifu = viewModel.getUser()?.waifu ?: return\n                    openCharacterDetailsBottomSheet(waifu)\n                }\n            }\n            layoutWaifuRow.root.setOnClickListener(onWaifuClicked)\n            layoutWaifuRow.tvValue.setOnClickListener(onWaifuClicked)\n\n            btnFollowing.setOnClickListener {\n                openProfilePageInWebView(\"following\")\n            }\n            btnFollowers.setOnClickListener {\n                openProfilePageInWebView(\"followers\")\n            }\n        }\n\n        initStatsViewPager()\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                val savedState = findNavController().currentBackStackEntry?.savedStateHandle\n                savedState?.getStateFlow(\"refreshFavorites\", false)\n                    ?.collectLatest { shouldRefresh ->\n                        if (shouldRefresh) {\n                            viewModel.refreshUser()\n                            savedState[\"refreshFavorites\"] = false\n                        }\n                    }\n            }\n        }\n    }\n\n    private fun initToolbar() {\n        binding.apply {\n            collapsingToolbar.initWindowInsetsListener(consume = false)\n            toolbar.initWindowInsetsListener(consume = false)\n            toolbar.setOnMenuItemClickListener { item ->\n                when (item.itemId) {\n                    R.id.menu_settings -> {\n                        val action = ProfileFragmentDirections\n                            .actionProfileFragmentToSettingsNavGraph()\n                        findNavController().navigate(action)\n                    }\n\n                    R.id.menu_edit_profile -> {\n                        val action = ProfileFragmentDirections\n                            .actionProfileFragmentToEditProfileFragment()\n                        findNavController().navigateSafe(R.id.profile_fragment, action)\n                    }\n\n                    R.id.menu_share_profile_url -> {\n                        val user = viewModel.getUser()\n                        val profileId = user?.slug ?: user?.id\n                        if (profileId != null) {\n                            val url = Kitsu.USER_URL_PREFIX + profileId\n                            startUrlShareIntent(url)\n                        } else {\n                            showSomethingWrongToast()\n                        }\n                    }\n\n                    R.id.menu_log_out -> {\n                        showLogOutConfirmationDialog()\n                    }\n                }\n                true\n            }\n        }\n    }\n\n    private fun setToolbarLogoClickListener() {\n        binding.toolbar.children.firstOrNull { it is ImageView }?.setOnClickListener { logoView ->\n            val avatarImgUrl = viewModel.getUser()?.avatar?.originalOrDown()\n                ?: return@setOnClickListener\n            val title = viewModel.getUser()?.name?.let { \"$it Avatar\" }\n            openPhotoViewActivity(avatarImgUrl, title, null, logoView)\n        }\n    }\n\n    private fun updateUser(user: User?) {\n        binding.user = user\n        binding.invalidateAll()\n\n        val glide = Glide.with(this)\n\n        glide.load(user?.avatar?.originalOrDown())\n            .dontAnimate()\n            .circleCrop()\n            .override(45.toPx())\n            .placeholder(R.drawable.profile_picture_placeholder)\n            .into(object : CustomTarget<Drawable>() {\n                override fun onResourceReady(\n                    resource: Drawable,\n                    transition: Transition<in Drawable>?\n                ) {\n                    binding.toolbar.logo = resource\n                    setToolbarLogoClickListener()\n                }\n\n                override fun onLoadCleared(placeholder: Drawable?) {}\n            })\n\n        glide.load(user?.coverImage?.originalOrDown())\n            .centerCrop()\n            .placeholder(ColorDrawable(SurfaceColors.SURFACE_0.getColor(requireContext())))\n            .into(binding.ivCover)\n\n        user?.waifu?.let { waifu ->\n            glide.asBitmap()\n                .load(waifu.image?.originalOrDown())\n                .circleCrop()\n                .dontAnimate()\n                .into(object : CustomTarget<Bitmap>() {\n                    override fun onResourceReady(\n                        resource: Bitmap,\n                        transition: Transition<in Bitmap>?\n                    ) {\n                        binding.layoutWaifuRow.icon = BitmapDrawable(resources, resource)\n                    }\n\n                    override fun onLoadCleared(placeholder: Drawable?) {}\n                })\n        }\n\n        user?.favorites?.let { updateFavoritesData(it) }\n    }\n\n    private fun initStatsViewPager() {\n        val dataSet = listOf(\n            ProfileStatsAdapter.ProfileStatsData(getString(R.string.profile_anime_stats)),\n            ProfileStatsAdapter.ProfileStatsData(getString(R.string.profile_manga_stats))\n        )\n        val adapter = ProfileStatsAdapter(dataSet)\n\n        binding.viewPagerStats.apply {\n            this.adapter = adapter\n            recyclerView.isNestedScrollingEnabled = false\n        }\n\n        TabLayoutMediator(binding.tabLayoutStats, binding.viewPagerStats) { tab, position ->\n            when (position) {\n                ProfileStatsAdapter.POS_ANIME -> tab.setText(R.string.profile_anime_stats)\n                ProfileStatsAdapter.POS_MANGA -> tab.setText(R.string.profile_manga_stats)\n            }\n        }.attach()\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.userModel.collectLatest { user ->\n                val animeCategoryStats: UserStatsData.CategoryBreakdownData? = user?.stats\n                    .findStatsData(UserStatsKind.AnimeCategoryBreakdown)\n                updateStatsChart(\n                    ProfileStatsAdapter.POS_ANIME,\n                    R.string.profile_anime_stats,\n                    animeCategoryStats\n                )\n\n                val mangaCategoryStats: UserStatsData.CategoryBreakdownData? = user?.stats\n                    .findStatsData(UserStatsKind.MangaCategoryBreakdown)\n                updateStatsChart(\n                    ProfileStatsAdapter.POS_MANGA,\n                    R.string.profile_manga_stats,\n                    mangaCategoryStats\n                )\n\n                val animeAmountConsumed: UserStatsData.AmountConsumedData? = user?.stats\n                    .findStatsData(UserStatsKind.AnimeAmountConsumed)\n                adapter.updateAmountConsumedData(ProfileStatsAdapter.POS_ANIME, animeAmountConsumed)\n\n                val mangaAmountConsumed: UserStatsData.AmountConsumedData? = user?.stats\n                    .findStatsData(UserStatsKind.MangaAmountConsumed)\n                adapter.updateAmountConsumedData(ProfileStatsAdapter.POS_MANGA, mangaAmountConsumed)\n            }\n        }\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.uiState.collectLatest { state ->\n                adapter.setLoading(ProfileStatsAdapter.POS_ANIME, state.isInitialLoading)\n                adapter.setLoading(ProfileStatsAdapter.POS_MANGA, state.isInitialLoading)\n            }\n        }\n    }\n\n    private inline fun <reified T> List<UserStats>?.findStatsData(kind: UserStatsKind): T? {\n        return this?.find { it.kind == kind }?.statsData as? T\n    }\n\n    private fun updateStatsChart(\n        position: Int,\n        @StringRes titleRes: Int,\n        categoryStats: UserStatsData.CategoryBreakdownData?\n    ) {\n        val categoryEntries: List<PieEntry> = categoryStats?.let { stats ->\n            val total = stats.total ?: return@let null\n            val categories = stats.categories ?: return@let null\n            categories.toList()\n                .filter { it.second != 0 }\n                .sortedByDescending { it.second }\n                .take(PieChartStyle.STATS_MAX_ELEMENTS)\n                .map { (category, value) ->\n                    PieEntry(\n                        round(value.toFloat() / total * 100f),\n                        category\n                    )\n                }\n        } ?: emptyList()\n\n        val set = PieDataSet(categoryEntries, getString(titleRes))\n\n        val adapter = binding.viewPagerStats.adapter as ProfileStatsAdapter\n        adapter.updateCategoryData(position, set)\n    }\n\n    private fun updateProfileLinks(profileLinks: List<ProfileLink>) {\n        binding.scrollViewProfileLinks.isVisible = profileLinks.isNotEmpty()\n        binding.chipGroupProfileLinks.apply {\n            removeAllViews()\n\n            profileLinks.sortedBy { it.profileLinkSite?.id?.toIntOrNull() }\n                .forEach { profileLink ->\n                    val profileLinkBinding = ItemProfileSiteChipBinding.inflate(\n                        layoutInflater,\n                        this,\n                        true\n                    )\n                    val chip = profileLinkBinding.root\n                    val siteName = profileLink.profileLinkSite?.name\n                    chip.text = siteName\n                    chip.setChipIconResource(getProfileSiteLogoResourceId(siteName))\n                    chip.setOnClickListener {\n                        onProfileLinkClicked(profileLink)\n                    }\n                }\n        }\n    }\n\n    private fun updateFavoritesData(favorites: List<Favorite>) {\n        val favAnime = favorites.filter { it.item is Anime }.map { it.item as Anime }\n        val favManga = favorites.filter { it.item is Manga }.map { it.item as Manga }\n        val favCharacters =\n            favorites.filter { it.item is Character }.map { it.item as Character }\n\n        showFavoriteMediaInRecyclerView(binding.rvFavoriteAnime, favAnime)\n        showFavoriteMediaInRecyclerView(binding.rvFavoriteManga, favManga)\n        showFavoriteCharactersInRecyclerView(binding.rvFavoriteCharacters, favCharacters)\n\n        binding.layoutFavoriteAnime.isVisible = favAnime.isNotEmpty()\n        binding.layoutFavoriteManga.isVisible = favManga.isNotEmpty()\n        binding.layoutFavoriteCharacters.isVisible = favCharacters.isNotEmpty()\n    }\n\n    private fun showFavoriteMediaInRecyclerView(\n        recyclerView: RecyclerView,\n        data: List<Media>\n    ) {\n        if (recyclerView.adapter !is MediaRecyclerViewAdapter) {\n            val glide = Glide.with(this)\n            val adapter = MediaRecyclerViewAdapter(\n                CopyOnWriteArrayList(data),\n                glide,\n                itemSize = MediaItemSize.SMALL\n            ) { view, media ->\n                onFavoriteMediaItemClicked(view, media)\n            }\n            recyclerView.adapter = adapter\n        } else {\n            val adapter = recyclerView.adapter as MediaRecyclerViewAdapter\n            adapter.dataSet.clear()\n            adapter.dataSet.addAll(data)\n            adapter.notifyDataSetChanged()\n        }\n    }\n\n    private fun showFavoriteCharactersInRecyclerView(\n        recyclerView: RecyclerView,\n        data: List<Character>\n    ) {\n        if (recyclerView.adapter !is CharacterAdapter) {\n            val glide = Glide.with(this)\n            val adapter = CharacterAdapter(\n                CopyOnWriteArrayList(data),\n                glide,\n            ) { _, character ->\n                openCharacterDetailsBottomSheet(character)\n            }\n            recyclerView.adapter = adapter\n        } else {\n            val adapter = recyclerView.adapter as CharacterAdapter\n            adapter.dataSet.clear()\n            adapter.dataSet.addAll(data)\n            adapter.notifyDataSetChanged()\n        }\n    }\n\n    private fun onFavoriteMediaItemClicked(view: View, media: Media) {\n        val action =\n            ProfileFragmentDirections.actionProfileFragmentToDetailsFragment(media.toMediaDto())\n        val detailsTransitionName = getString(R.string.details_poster_transition_name)\n        val extras = FragmentNavigatorExtras(view to detailsTransitionName)\n        findNavController().navigateSafe(R.id.profile_fragment, action, extras)\n    }\n\n    private fun openCharacterDetailsBottomSheet(character: Character) {\n        val action =\n            ProfileFragmentDirections.actionProfileFragmentToCharacterDetailsBottomSheet(\n                character.toCharacterDto()\n            )\n        findNavController().navigateSafe(R.id.profile_fragment, action)\n    }\n\n    private fun updateOptionsMenu() {\n        val isLoggedIn = viewModel.getUser() != null\n        binding.toolbar.menu.apply {\n            findItem(R.id.menu_edit_profile).isVisible = isLoggedIn\n            findItem(R.id.menu_log_out).isVisible = isLoggedIn\n            findItem(R.id.menu_share_profile_url).isVisible = isLoggedIn\n        }\n    }\n\n    private fun showLogOutConfirmationDialog() {\n        MaterialAlertDialogBuilder(requireContext())\n            .setTitle(R.string.action_log_out)\n            .setMessage(R.string.dialog_log_out_confirmation)\n            .setNegativeButton(android.R.string.cancel) { dialog, _ ->\n                dialog.dismiss()\n            }\n            .setPositiveButton(R.string.action_log_out) { dialog, _ ->\n                onLogOut()\n                dialog.dismiss()\n            }\n            .show()\n    }\n\n    private fun onLogOut() {\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.logOut()\n        }\n    }\n\n    private fun onProfileLinkClicked(profileLink: ProfileLink) {\n        profileLink.url?.let { url ->\n            if (URLUtil.isValidUrl(url)) {\n                openUrl(url)\n            } else {\n                copyToClipboard(profileLink.profileLinkSite?.name ?: \"URL\", url)\n            }\n        }\n    }\n\n    private fun openProfilePageInWebView(subPage: String) {\n        val user = viewModel.getUser() ?: return\n        val url = \"https://kitsu.app/users/${user.slug ?: user.id}/$subPage\"\n        val webViewAction = WebViewFragmentDirections.actionGlobalWebViewFragment(url)\n        findNavController().navigateSafe(R.id.profile_fragment, webViewAction)\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        binding.nsvContent.smoothScrollTo(0, 0)\n        binding.appBarLayout.setExpanded(true)\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/profile/ProfileStatsAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.profile\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.core.text.HtmlCompat\nimport androidx.core.view.isVisible\nimport androidx.recyclerview.widget.RecyclerView\nimport com.github.mikephil.charting.data.PieData\nimport com.github.mikephil.charting.data.PieDataSet\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.user.stats.UserStatsData\nimport io.github.drumber.kitsune.databinding.ItemProfileStatsBinding\nimport io.github.drumber.kitsune.ui.component.chart.PieChartStyle.applyStyle\nimport io.github.drumber.kitsune.util.TimeUtil\nimport kotlin.math.roundToInt\n\nclass ProfileStatsAdapter(dataSet: List<ProfileStatsData>) :\n    RecyclerView.Adapter<ProfileStatsAdapter.ProfileStatsViewHolder>() {\n\n    companion object {\n        const val POS_ANIME = 0\n        const val POS_MANGA = 1\n    }\n\n    private val dataSet = dataSet.toMutableList()\n\n    fun updateCategoryData(position: Int, data: PieDataSet) {\n        if (dataSet[position].categoriesDataSet == data) return\n        dataSet[position].categoriesDataSet = data\n        notifyItemChanged(position)\n    }\n\n    fun updateAmountConsumedData(position: Int, data: UserStatsData.AmountConsumedData?) {\n        if (dataSet[position].amountConsumedData == data) return\n        dataSet[position].amountConsumedData = data\n        notifyItemChanged(position)\n    }\n\n    fun setLoading(position: Int, isLoading: Boolean) {\n        if (dataSet[position].isLoading == isLoading) return\n        dataSet[position].isLoading = isLoading\n        notifyItemChanged(position)\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileStatsViewHolder {\n        val binding = ItemProfileStatsBinding\n            .inflate(LayoutInflater.from(parent.context), parent, false)\n        return ProfileStatsViewHolder(binding)\n    }\n\n    override fun onBindViewHolder(holder: ProfileStatsViewHolder, position: Int) {\n        holder.bind(dataSet[position])\n    }\n\n    override fun getItemCount() = dataSet.size\n\n    inner class ProfileStatsViewHolder(private val binding: ItemProfileStatsBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        init {\n            binding.apply {\n                pieChart.applyStyle(binding.root.context)\n                progressBar.isVisible = true\n            }\n        }\n\n        private val isAnime get() = bindingAdapterPosition == POS_ANIME\n        private val isManga get() = bindingAdapterPosition == POS_MANGA\n\n        fun bind(dataModel: ProfileStatsData) {\n            val context = binding.root.context\n            updateCategoryChart(dataModel)\n\n            binding.apply {\n                progressBar.isVisible = dataModel.isLoading\n\n                dataModel.amountConsumedData?.let { stats ->\n                    if (isAnime) {\n                        stats.time?.let { time ->\n                            tvTimeSpent.text = context.getString(\n                                R.string.profile_stats_anime_watch_time,\n                                TimeUtil.roundTime(time, context, 1)\n                            )\n\n                            if (time > 0) {\n                                tvTimeSpentTotal.text = context.getString(\n                                    R.string.profile_stats_time_spent_total,\n                                    TimeUtil.timeToHumanReadableFormat(time, context)\n                                )\n                            }\n                        }\n                    } else if (isManga) {\n                        tvTimeSpent.text = stats.units?.let { chapters ->\n                            context.getString(R.string.profile_stats_manga_chapters_read, chapters)\n                        }\n                    }\n\n                    val completed = stats.completed\n                    val percentiles = if (isAnime) {\n                        stats.percentiles?.time\n                    } else {\n                        stats.percentiles?.units\n                    }\n                    val htmlText = if (completed != null && percentiles != null) {\n                        context.getString(\n                            R.string.profile_stats_completed,\n                            completed,\n                            percentiles.times(100).roundToInt().coerceIn(0..99)\n                        )\n                    } else {\n                        null\n                    }\n                    tvCompleted.text = htmlText?.let {\n                        HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY)\n                    }\n                }\n            }\n        }\n\n        private fun updateCategoryChart(dataModel: ProfileStatsData) {\n            val context = binding.root.context\n\n            val isDataEmpty = dataModel.categoriesDataSet.let { it == null || it.values.isEmpty() }\n            binding.apply {\n                pieChart.isVisible = !isDataEmpty && !dataModel.isLoading\n                tvCategoriesNoData.isVisible = isDataEmpty && !dataModel.isLoading\n            }\n\n            dataModel.categoriesDataSet?.let { set ->\n                set.applyStyle(context)\n\n                val pieData = PieData(set)\n                pieData.applyStyle(context)\n\n                binding.pieChart.apply {\n                    data = pieData\n                    centerText = dataModel.title\n                    invalidate()\n                }\n            } ?: binding.pieChart.clear()\n        }\n    }\n\n    data class ProfileStatsData(\n        val title: String,\n        var categoriesDataSet: PieDataSet? = null,\n        var amountConsumedData: UserStatsData.AmountConsumedData? = null,\n        var isLoading: Boolean = true\n    )\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/profile/ProfileViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.profile\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.constants.Defaults\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.data.mapper.UserMapper.toUser\nimport io.github.drumber.kitsune.data.presentation.model.user.User\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.domain.auth.LogOutUserUseCase\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass ProfileViewModel(\n    private val userRepository: UserRepository,\n    private val logOutUser: LogOutUserUseCase\n) : ViewModel() {\n\n    private val _refreshTrigger = MutableSharedFlow<Unit>()\n\n    private val _userModel = combine(\n        userRepository.localUser,\n        _refreshTrigger.onStart { emit(Unit) }\n    ) { user, _ ->\n            try {\n                user?.let { fetchFullUserModel(it.id) ?: user.toUser() }\n            } finally {\n                _uiState.update {\n                    it.copy(\n                        isInitialLoading = false,\n                        isRefreshing = false\n                    )\n                }\n            }\n        }.stateIn(\n            scope = viewModelScope,\n            started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 60000L),\n            initialValue = userRepository.localUser.value?.toUser()\n        )\n\n    val userModel = _userModel\n\n    private val _uiState = MutableStateFlow(ProfileUiState())\n    val uiState = _uiState.asStateFlow()\n\n    fun getUser(): User? {\n        return _userModel.replayCache.firstOrNull() ?: userRepository.localUser.value?.toUser()\n    }\n\n    fun refreshUser() {\n        viewModelScope.launch {\n            _uiState.update { it.copy(isRefreshing = true) }\n            _refreshTrigger.emit(Unit)\n        }\n    }\n\n    private suspend fun fetchFullUserModel(userId: String): User? {\n        return try {\n            userRepository.fetchUser(userId, FULL_USER_FILTER)\n                ?: throw NoDataException(\"Received data is null.\")\n        } catch (e: Exception) {\n            logE(\"Failed to fetch full user model.\", e)\n            null\n        }\n    }\n\n    suspend fun logOut() {\n        viewModelScope.async {\n            logOutUser()\n        }.await()\n    }\n\n    companion object {\n        val FULL_USER_FILTER\n            get() = Filter()\n                .include(\"stats\", \"favorites.item\", \"waifu\", \"profileLinks.profileLinkSite\")\n                .fields(\"media\", *Defaults.MINIMUM_COLLECTION_FIELDS)\n                .fields(\"characters\", *Defaults.MINIMUM_CHARACTER_FIELDS)\n    }\n}\n\ndata class ProfileUiState(\n    val isRefreshing: Boolean = false,\n    val isInitialLoading: Boolean = true\n)"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/profile/editprofile/CharacterSearchResultAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.profile.editprofile\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.recyclerview.widget.RecyclerView\nimport com.algolia.instantsearch.core.hits.HitsView\nimport com.bumptech.glide.Glide\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.character.CharacterSearchResult\nimport io.github.drumber.kitsune.databinding.ItemCharacterSearchResultBinding\nimport io.github.drumber.kitsune.util.fixImageUrl\n\nclass CharacterSearchResultAdapter(private val onCharacterClicked: (CharacterSearchResult) -> Unit) :\n    RecyclerView.Adapter<CharacterSearchResultAdapter.CharacterSearchResultViewHolder>(),\n    HitsView<CharacterSearchResult> {\n\n    private val characters = mutableListOf<CharacterSearchResult>()\n\n    override fun onCreateViewHolder(\n        parent: ViewGroup,\n        viewType: Int\n    ): CharacterSearchResultViewHolder {\n        return CharacterSearchResultViewHolder(\n            ItemCharacterSearchResultBinding.inflate(\n                LayoutInflater.from(parent.context),\n                parent,\n                false\n            )\n        )\n    }\n\n    override fun onBindViewHolder(holder: CharacterSearchResultViewHolder, position: Int) {\n        holder.bind(characters[position])\n    }\n\n    override fun getItemCount(): Int = characters.size\n\n    override fun setHits(hits: List<CharacterSearchResult>) {\n        characters.clear()\n        characters.addAll(hits)\n        notifyDataSetChanged()\n    }\n\n    inner class CharacterSearchResultViewHolder(private val binding: ItemCharacterSearchResultBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        fun bind(character: CharacterSearchResult) {\n            binding.apply {\n                tvName.text = character.name\n                tvMedia.text = character.primaryMediaTitle\n                tvMedia.isVisible = !character.primaryMediaTitle.isNullOrBlank()\n\n                Glide.with(root)\n                    .load(character.image?.originalOrDown()?.fixImageUrl())\n                    .placeholder(R.drawable.character_placeholder)\n                    .into(ivCharacter)\n\n                root.setOnClickListener {\n                    onCharacterClicked(character)\n                }\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/profile/editprofile/EditProfileFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.profile.editprofile\n\nimport android.net.Uri\nimport android.os.Bundle\nimport android.util.Base64\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.WindowManager\nimport android.widget.Toast\nimport androidx.activity.ComponentDialog\nimport androidx.activity.addCallback\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.activity.result.PickVisualMediaRequest\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.core.os.BundleCompat\nimport androidx.core.os.bundleOf\nimport androidx.core.view.isVisible\nimport androidx.core.view.postDelayed\nimport androidx.core.widget.doAfterTextChanged\nimport androidx.lifecycle.lifecycleScope\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.algolia.instantsearch.android.searchbox.SearchBoxViewEditText\nimport com.algolia.instantsearch.core.connection.ConnectionHandler\nimport com.algolia.instantsearch.core.hits.connectHitsView\nimport com.algolia.instantsearch.searchbox.connectView\nimport com.algolia.search.helper.deserialize\nimport com.bumptech.glide.Glide\nimport com.google.android.material.datepicker.MaterialDatePicker\nimport com.google.android.material.search.SearchView\nimport com.google.android.material.textfield.MaterialAutoCompleteTextView\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.mapper.AlgoliaMapper.toCharacterSearchResult\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toCharacter\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLinkSite\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaCharacterSearchResult\nimport io.github.drumber.kitsune.databinding.FragmentEditProfileBinding\nimport io.github.drumber.kitsune.databinding.ItemProfileSiteChipBinding\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.base.BaseDialogFragment\nimport io.github.drumber.kitsune.util.DataUtil\nimport io.github.drumber.kitsune.util.formatDate\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.parseDate\nimport io.github.drumber.kitsune.util.toDate\nimport io.github.drumber.kitsune.util.ui.getProfileSiteLogoResourceId\nimport io.github.drumber.kitsune.util.ui.initImePaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initMarginWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass EditProfileFragment : BaseDialogFragment(R.layout.fragment_edit_profile) {\n\n    private var _binding: FragmentEditProfileBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: EditProfileViewModel by viewModel()\n\n    private val connectionHandler = ConnectionHandler()\n\n    private lateinit var pickImage: ActivityResultLauncher<PickVisualMediaRequest>\n    private lateinit var legacyGetContent: ActivityResultLauncher<String>\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        pickImage = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->\n            onImageUriSelected(uri)\n        }\n\n        legacyGetContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->\n            onImageUriSelected(uri)\n        }\n    }\n\n    override fun onStart() {\n        requireDialog().window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)\n        super.onStart()\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentEditProfileBinding.inflate(inflater, container, false)\n\n        binding.toolbar.initWindowInsetsListener(consume = false)\n        binding.nestedScrollView.initMarginWindowInsetsListener(\n            left = true,\n            right = true,\n            bottom = true,\n            consume = false\n        )\n\n        if (!viewModel.hasUser()) {\n            Toast.makeText(requireContext(), R.string.error_invalid_user, Toast.LENGTH_LONG).show()\n            dismiss()\n        }\n\n        binding.root.initImePaddingWindowInsetsListener()\n\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        childFragmentManager.setFragmentResultListener(\n            SelectProfileLinkSiteBottomSheet.PROFILE_SITE_SELECTED_REQUEST_KEY,\n            this\n        ) { _, bundle ->\n            val linkSite = BundleCompat.getParcelable(\n                bundle,\n                SelectProfileLinkSiteBottomSheet.BUNDLE_PROFILE_LINK_SITE,\n                ProfileLinkSite::class.java\n            ) ?: return@setFragmentResultListener\n\n            openEditProfileLinkBottomSheet(ProfileLinkEntry(null, \"\", linkSite), true)\n        }\n\n        childFragmentManager.setFragmentResultListener(\n            EditProfileLinkBottomSheet.PROFILE_SUCCESS_REQUEST_KEY,\n            this\n        ) { _, bundle ->\n            val profileLinkEntry = BundleCompat.getParcelable(\n                bundle,\n                EditProfileLinkBottomSheet.BUNDLE_PROFILE_LINK_ENTRY,\n                ProfileLinkEntry::class.java\n            ) ?: return@setFragmentResultListener\n\n            viewModel.acceptProfileLinkAction(ProfileLinkAction.Edit(profileLinkEntry))\n        }\n\n        childFragmentManager.setFragmentResultListener(\n            EditProfileLinkBottomSheet.PROFILE_DELETE_REQUEST_KEY,\n            this\n        ) { _, bundle ->\n            val profileLinkEntry = BundleCompat.getParcelable(\n                bundle,\n                EditProfileLinkBottomSheet.BUNDLE_PROFILE_LINK_ENTRY,\n                ProfileLinkEntry::class.java\n            ) ?: return@setFragmentResultListener\n\n            viewModel.acceptProfileLinkAction(ProfileLinkAction.Delete(profileLinkEntry))\n        }\n\n        binding.apply {\n            toolbar.setNavigationOnClickListener { dismiss() }\n\n            cardAvatar.setOnClickListener {\n                openImagePicker(ImagePickerType.AVATAR)\n            }\n\n            cardCover.setOnClickListener {\n                openImagePicker(ImagePickerType.COVER)\n            }\n\n            chipAddProfileLink.setOnClickListener { openSelectProfileLinkSiteBottomSheet() }\n\n            fieldLocation.editText?.apply {\n                setText(viewModel.profileState.location)\n                doAfterTextChanged {\n                    viewModel.acceptProfileChanges(\n                        viewModel.profileState.copy(\n                            location = it?.trim().toString()\n                        )\n                    )\n                }\n            }\n\n            fieldBirthday.editText?.setOnClickListener {\n                val selectedDate = viewModel.profileState.birthday.parseDate()?.time\n                    ?: MaterialDatePicker.todayInUtcMilliseconds()\n\n                openDatePicker(selectedDate, getString(R.string.profile_data_birthday)) { date ->\n                    val dateString = date.toDate().formatDate(\"yyyy-MM-dd\")\n                    viewModel.acceptProfileChanges(\n                        viewModel.profileState.copy(birthday = dateString)\n                    )\n                }\n            }\n            fieldBirthday.setEndIconOnClickListener {\n                if (viewModel.profileState.birthday.isEmpty()) {\n                    fieldBirthday.editText?.performClick()\n                } else {\n                    viewModel.acceptProfileChanges(\n                        viewModel.profileState.copy(birthday = \"\")\n                    )\n                }\n            }\n\n            (menuGender.editText as? MaterialAutoCompleteTextView)?.apply {\n                val genderItems = arrayOf(\n                    R.string.profile_data_private,\n                    R.string.profile_gender_male,\n                    R.string.profile_gender_female,\n                    R.string.profile_gender_custom\n                ).map { getString(it) }.toTypedArray()\n                setSimpleItems(genderItems)\n                setText(\n                    DataUtil.getGenderString(viewModel.profileState.gender, requireContext()),\n                    false\n                )\n\n                setOnItemClickListener { _, _, position, _ ->\n                    val gender = when (position) {\n                        0 -> \"secret\"\n                        1 -> \"male\"\n                        2 -> \"female\"\n                        else -> \"custom\"\n                    }\n                    val customGender =\n                        if (gender != \"custom\") \"\" else viewModel.profileState.customGender\n                    viewModel.acceptProfileChanges(\n                        viewModel.profileState.copy(\n                            gender = gender,\n                            customGender = customGender\n                        )\n                    )\n                    binding.fieldCustomGender.editText?.setText(customGender)\n                    if (gender == \"custom\") {\n                        postDelayed(100) {\n                            binding.fieldCustomGender.editText?.requestFocus()\n                        }\n                    }\n                }\n            }\n\n            fieldCustomGender.editText?.setText(viewModel.profileState.customGender)\n            fieldCustomGender.editText?.doAfterTextChanged {\n                viewModel.acceptProfileChanges(\n                    viewModel.profileState.copy(customGender = it?.trim().toString())\n                )\n            }\n\n            (menuWaifu.editText as? MaterialAutoCompleteTextView)?.apply {\n                val waifuItems = arrayOf(\n                    \"\",\n                    getString(R.string.profile_data_waifu),\n                    getString(R.string.profile_data_husbando)\n                )\n                setSimpleItems(waifuItems)\n                setText(viewModel.profileState.waifuOrHusbando, false)\n\n                setOnItemClickListener { _, _, position, _ ->\n                    val waifuOrHusbando = waifuItems[position]\n                    viewModel.acceptProfileChanges(\n                        viewModel.profileState.copy(waifuOrHusbando = waifuOrHusbando)\n                    )\n                }\n            }\n\n            fieldSearchWaifu.editText?.setOnClickListener {\n                viewModel.initSearchClient()\n                characterSearchView.clearText()\n                characterSearchView.show()\n            }\n            fieldSearchWaifu.setEndIconOnClickListener {\n                if (viewModel.profileState.character != null) {\n                    viewModel.acceptProfileChanges(\n                        viewModel.profileState.copy(character = null)\n                    )\n                } else {\n                    fieldSearchWaifu.editText?.performClick()\n                }\n            }\n\n            fieldBio.editText?.apply {\n                setText(viewModel.profileState.about)\n                doAfterTextChanged {\n                    viewModel.acceptProfileChanges(\n                        viewModel.profileState.copy(about = it?.trim().toString())\n                    )\n                }\n            }\n\n            btnUpdateProfile.setOnClickListener {\n                viewModel.updateUserProfile(createUserImageUpload())\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.profileStateFlow.collectLatest { profileState ->\n                binding.apply {\n                    fieldBirthday.editText?.setText(\n                        profileState.birthday.parseDate()?.formatDate()\n                    )\n                    fieldBirthday.setEndIconDrawable(\n                        if (profileState.birthday.isEmpty()) {\n                            R.drawable.ic_calendar_month_24\n                        } else {\n                            R.drawable.ic_close_24\n                        }\n                    )\n\n                    fieldCustomGender.isVisible = profileState.gender == \"custom\"\n\n                    fieldSearchWaifu.apply {\n                        isVisible = profileState.waifuOrHusbando.isNotBlank()\n                        editText?.setText(profileState.character?.name)\n                        setEndIconDrawable(\n                            if (profileState.character == null) {\n                                R.drawable.ic_search_24\n                            } else {\n                                R.drawable.ic_heart_broken_24\n                            }\n                        )\n                    }\n                }\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.profileImageStateFlow.collectLatest { profileImageState ->\n                val avatarImage = profileImageState.selectedAvatarUri\n                    ?: profileImageState.currentAvatarUrl\n                Glide.with(this@EditProfileFragment)\n                    .load(avatarImage)\n                    .placeholder(R.drawable.profile_picture_placeholder)\n                    .into(binding.ivAvatar)\n\n                val coverImage = profileImageState.selectedCoverUri\n                    ?: profileImageState.currentCoverUrl\n                Glide.with(this@EditProfileFragment)\n                    .load(coverImage)\n                    .placeholder(R.drawable.cover_placeholder)\n                    .into(binding.ivCover)\n\n                binding.ivCoverAddImage.isVisible = coverImage == null\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.profileLinkEntriesFlow.collectLatest { profileLinks ->\n                val indexOfAddChip = binding.chipGroupProfileLinks\n                    .indexOfChild(binding.chipAddProfileLink)\n                if (indexOfAddChip != -1) {\n                    binding.chipGroupProfileLinks.removeViews(0, indexOfAddChip)\n                }\n\n                profileLinks.sortedByDescending { it.site.id?.toIntOrNull() }.forEach { link ->\n                    val chipBinding = ItemProfileSiteChipBinding.inflate(\n                        layoutInflater,\n                        binding.chipGroupProfileLinks,\n                        false\n                    )\n                    val chip = chipBinding.root\n                    chip.text = link.site.name\n                    chip.setChipIconResource(getProfileSiteLogoResourceId(link.site.name))\n                    chip.setOnClickListener {\n                        openEditProfileLinkBottomSheet(link, false)\n                    }\n                    binding.chipGroupProfileLinks.addView(chip, 0)\n                }\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.canUpdateProfileFlow.collectLatest { canUpdate ->\n                binding.btnUpdateProfile.isEnabled = canUpdate\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.loadingStateFlow.collectLatest { loadingState ->\n                binding.layoutLoading.isVisible = loadingState is LoadingState.Loading\n\n                if (loadingState is LoadingState.Error && !loadingState.isConsumed) {\n                    loadingState.isConsumed = true\n\n                    if (loadingState.exception is ProfileUpdateException) {\n                        showErrorToUser(loadingState.exception)\n                    } else {\n                        Toast.makeText(\n                            requireContext(),\n                            R.string.error_user_update_failed,\n                            Toast.LENGTH_LONG\n                        ).show()\n                    }\n                } else if (loadingState is LoadingState.Success) {\n                    dismiss()\n                }\n            }\n        }\n\n        initSearchView()\n    }\n\n    private fun initSearchView() {\n        val adapter = CharacterSearchResultAdapter {\n            viewModel.acceptProfileChanges(viewModel.profileState.copy(character = it.toCharacter()))\n            binding.characterSearchView.hide()\n        }\n\n        binding.rvCharacterResults.apply {\n            initPaddingWindowInsetsListener(\n                left = true,\n                right = true,\n                bottom = true,\n                consume = false\n            )\n            this.adapter = adapter\n            layoutManager =\n                LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)\n        }\n\n        adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {\n            override fun onChanged() {\n                binding.rvCharacterResults.scrollToPosition(0)\n            }\n        })\n\n        binding.characterSearchView.editText.doAfterTextChanged { text ->\n            if (text.isNullOrBlank()) {\n                adapter.setHits(emptyList())\n            }\n        }\n\n        val backPressedCallback = (dialog as ComponentDialog).onBackPressedDispatcher\n            .addCallback(this, false) {\n                binding.characterSearchView.hide()\n                isEnabled = false\n            }\n\n        binding.characterSearchView.addTransitionListener { _, _, newState ->\n            backPressedCallback.isEnabled = newState == SearchView.TransitionState.SHOWN ||\n                    newState == SearchView.TransitionState.SHOWING\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.searchBoxConnectorFlow.collectLatest { searchBox ->\n                connectionHandler.clear()\n                val searchBoxView = SearchBoxViewEditText(binding.characterSearchView.editText)\n                connectionHandler += searchBox.connectView(searchBoxView)\n                connectionHandler += searchBox.searcher.connectHitsView(adapter) { response ->\n                    response.hits.deserialize(AlgoliaCharacterSearchResult.serializer()).map { it.toCharacterSearchResult() }\n                }\n            }\n        }\n    }\n\n    private fun openSelectProfileLinkSiteBottomSheet() {\n        val bottomSheet = SelectProfileLinkSiteBottomSheet()\n        bottomSheet.show(childFragmentManager, SelectProfileLinkSiteBottomSheet.TAG)\n    }\n\n    private fun openEditProfileLinkBottomSheet(\n        profileLinkEntry: ProfileLinkEntry,\n        isCreatingNew: Boolean\n    ) {\n        val bottomSheet = EditProfileLinkBottomSheet().apply {\n            arguments = bundleOf(\n                EditProfileLinkBottomSheet.BUNDLE_IS_CREATING_NEW to isCreatingNew,\n                EditProfileLinkBottomSheet.BUNDLE_PROFILE_LINK_ENTRY to profileLinkEntry\n            )\n        }\n        bottomSheet.show(childFragmentManager, EditProfileLinkBottomSheet.TAG)\n    }\n\n    private fun openDatePicker(selectedDate: Long, title: String, action: (Long) -> Unit) {\n        val datePicker = MaterialDatePicker.Builder.datePicker()\n            .setTitleText(title)\n            .setSelection(selectedDate)\n            .build()\n\n        datePicker.addOnPositiveButtonClickListener(action)\n        datePicker.show(parentFragmentManager, \"DATE_PICKER\")\n    }\n\n    private fun openImagePicker(type: ImagePickerType) {\n        viewModel.currentImagePickerType = type\n\n        if (!KitsunePref.forceLegacyImagePicker\n            && ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(requireContext())\n        ) {\n            pickImage.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))\n        } else {\n            legacyGetContent.launch(\"image/*\")\n        }\n    }\n\n    private fun onImageUriSelected(uri: Uri?) {\n        if (uri != null) {\n            val imageState = viewModel.profileImageState\n            val newImageState = when (viewModel.currentImagePickerType) {\n                ImagePickerType.AVATAR -> imageState.copy(selectedAvatarUri = uri)\n                ImagePickerType.COVER -> imageState.copy(selectedCoverUri = uri)\n                else -> imageState\n            }\n            viewModel.acceptProfileImageChanges(newImageState)\n        }\n        viewModel.currentImagePickerType = null\n    }\n\n    private fun createUserImageUpload(): ProfileImageContainer? {\n        val imageState = viewModel.profileImageState\n        val avatarUri = imageState.selectedAvatarUri\n        val coverUri = imageState.selectedCoverUri\n        if (avatarUri == null && coverUri == null) {\n            return null\n        }\n\n        val profileImages = ProfileImageContainer(\n            avatar = avatarUri?.let { getBase64ImageFrom(it) },\n            coverImage = coverUri?.let { getBase64ImageFrom(it) }\n        )\n\n        if (profileImages.avatar == null && profileImages.coverImage == null) {\n            return null\n        }\n        return profileImages\n    }\n\n    private fun getBase64ImageFrom(uri: Uri): String? {\n        val inputStream = requireContext().contentResolver.openInputStream(uri) ?: return null\n\n        // get mime type from image (default to jpeg)\n        val mimeType = requireContext().contentResolver.getType(uri) ?: \"image/jpeg\"\n\n        return try {\n            inputStream.use { stream ->\n                val bytes = stream.readBytes()\n                Base64.encodeToString(bytes, Base64.DEFAULT)\n            }.let { base64 ->\n                \"data:$mimeType;base64,$base64\"\n            }\n        } catch (e: Exception) {\n            logE(\"Error while encoding image to Base64 from uri: $uri\", e)\n            null\n        }\n    }\n\n    private fun showErrorToUser(profileUpdateException: ProfileUpdateException) {\n        val message = when (profileUpdateException) {\n            is ProfileUpdateException.ProfileDataError -> when (profileUpdateException.type) {\n                ProfileDataErrorType.UpdateProfile -> getString(R.string.error_user_update_failed)\n                ProfileDataErrorType.DeleteWaifu -> getString(R.string.error_user_delete_waifu_failed)\n            }\n\n            is ProfileUpdateException.ProfileImageError -> getString(R.string.error_user_update_image_failed)\n\n            is ProfileUpdateException.ProfileLinkError -> {\n                val siteName = profileUpdateException.profileLinkEntry.site.name\n                    ?: viewModel.profileLinkSites\n                        ?.find { it.id == profileUpdateException.profileLinkEntry.site.id }?.name\n                    ?: profileUpdateException.profileLinkEntry.url\n\n                when (profileUpdateException.operation) {\n                    ProfileLinkOperation.Create -> getString(\n                        R.string.error_user_create_profile_link_failed,\n                        siteName\n                    )\n\n                    ProfileLinkOperation.Update -> getString(\n                        R.string.error_user_update_profile_link_failed,\n                        siteName\n                    )\n\n                    ProfileLinkOperation.Delete -> getString(\n                        R.string.error_user_delete_profile_link_failed,\n                        siteName\n                    )\n                }\n            }\n        }\n\n        Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()\n    }\n\n    override fun onDestroyView() {\n        connectionHandler.clear()\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/profile/editprofile/EditProfileLinkBottomSheet.kt",
    "content": "package io.github.drumber.kitsune.ui.profile.editprofile\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.os.BundleCompat\nimport androidx.core.os.bundleOf\nimport androidx.core.widget.doOnTextChanged\nimport androidx.fragment.app.setFragmentResult\nimport com.google.android.material.bottomsheet.BottomSheetDialogFragment\nimport io.github.drumber.kitsune.databinding.SheetEditProfileLinkBinding\nimport io.github.drumber.kitsune.util.ui.getProfileSiteLogoResourceId\n\nclass EditProfileLinkBottomSheet : BottomSheetDialogFragment() {\n\n    private var _binding: SheetEditProfileLinkBinding? = null\n    private val binding get() = _binding!!\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = SheetEditProfileLinkBinding.inflate(inflater, container, false)\n        binding.isCreatingNew = isCreatingNew()\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        val profileLinkEntry = arguments?.let { bundle ->\n            BundleCompat.getParcelable(\n                bundle,\n                BUNDLE_PROFILE_LINK_ENTRY,\n                ProfileLinkEntry::class.java\n            )\n        } ?: return\n\n        fun isConfirmButtonEnabled(): Boolean {\n            val text = binding.fieldUrl.editText?.text?.toString()\n            return !text.isNullOrBlank() && text != profileLinkEntry.url\n        }\n\n        binding.apply {\n            profileLinkEntry.site.name?.let { siteName ->\n                ivLogo.setImageResource(getProfileSiteLogoResourceId(siteName))\n                tvSiteName.text = siteName\n            }\n\n            fieldUrl.editText?.setText(profileLinkEntry.url)\n            fieldUrl.editText?.doOnTextChanged { _, _, _, _ ->\n                btnConfirm.isEnabled = isConfirmButtonEnabled()\n            }\n\n            btnDelete.setOnClickListener {\n                setFragmentResult(\n                    PROFILE_DELETE_REQUEST_KEY,\n                    bundleOf(BUNDLE_PROFILE_LINK_ENTRY to profileLinkEntry)\n                )\n                dismiss()\n            }\n            btnConfirm.isEnabled = isConfirmButtonEnabled()\n            btnCancel.setOnClickListener { dismiss() }\n            btnConfirm.setOnClickListener {\n                val text = fieldUrl.editText?.text?.toString()\n                if (text.isNullOrBlank()) return@setOnClickListener\n\n                val editedProfileLinkEntry = profileLinkEntry.copy(url = text)\n                setFragmentResult(\n                    PROFILE_SUCCESS_REQUEST_KEY,\n                    bundleOf(BUNDLE_PROFILE_LINK_ENTRY to editedProfileLinkEntry)\n                )\n                dismiss()\n            }\n        }\n    }\n\n    private fun isCreatingNew() = arguments?.getBoolean(BUNDLE_IS_CREATING_NEW) == true\n\n    override fun onDestroy() {\n        super.onDestroy()\n        _binding = null\n    }\n\n    companion object {\n        const val TAG = \"edit_profile_link_bottom_sheet\"\n        const val BUNDLE_IS_CREATING_NEW = \"is_creating_new_bundle_key\"\n        const val BUNDLE_PROFILE_LINK_ENTRY = \"profile_link_entry_bundle_key\"\n        const val PROFILE_SUCCESS_REQUEST_KEY = \"edit_profile_link_success_request_key\"\n        const val PROFILE_DELETE_REQUEST_KEY = \"edit_profile_link_delete_request_key\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/profile/editprofile/EditProfileViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.profile.editprofile\n\nimport android.net.Uri\nimport android.os.Parcelable\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.algolia.instantsearch.core.connection.ConnectionHandler\nimport com.algolia.instantsearch.searchbox.SearchBoxConnector\nimport com.algolia.search.dsl.attributesToHighlight\nimport com.algolia.search.dsl.attributesToRetrieve\nimport com.algolia.search.dsl.query\nimport com.algolia.search.dsl.responseFields\nimport com.algolia.search.model.response.ResponseSearch\nimport com.algolia.search.model.search.Query\nimport com.algolia.search.model.search.RemoveStopWords\nimport com.algolia.search.model.search.RemoveWordIfNoResults\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toCharacter\nimport io.github.drumber.kitsune.data.presentation.model.algolia.SearchType\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLink\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLinkSite\nimport io.github.drumber.kitsune.data.repository.AlgoliaKeyRepository\nimport io.github.drumber.kitsune.data.repository.ProfileLinkRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.data.source.local.character.LocalCharacter\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.domain.algolia.SearchProvider\nimport io.github.drumber.kitsune.util.logD\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.parcelize.Parcelize\n\nclass EditProfileViewModel(\n    private val userRepository: UserRepository,\n    private val profileLinkRepository: ProfileLinkRepository,\n    algoliaKeyRepository: AlgoliaKeyRepository\n) : ViewModel() {\n\n    val loadingStateFlow: StateFlow<LoadingState>\n\n    val profileStateFlow: StateFlow<ProfileState>\n    val profileImageStateFlow: StateFlow<ProfileImageState>\n    val profileLinkEntriesFlow: StateFlow<List<ProfileLinkEntry>>\n    val canUpdateProfileFlow: Flow<Boolean>\n\n    private val initialProfileLinksFlow = MutableSharedFlow<List<ProfileLinkEntry>>(1)\n    private val _profileLinkSitesFlow = MutableSharedFlow<List<ProfileLinkSite>>(1)\n    private val _profileLinkSitesLoadStateFlow = MutableStateFlow(false)\n\n    val profileLinkSitesFlow\n        get() = _profileLinkSitesFlow.asSharedFlow()\n\n    val profileLinkSitesLoadStateFlow: StateFlow<Boolean>\n        get() = _profileLinkSitesLoadStateFlow.asStateFlow()\n\n    val profileState\n        get() = profileStateFlow.value\n\n    val profileImageState\n        get() = profileImageStateFlow.value\n\n    val profileLinkEntries\n        get() = profileLinkEntriesFlow.value\n\n    val profileLinkSites\n        get() = _profileLinkSitesFlow.replayCache.firstOrNull()\n\n    val acceptProfileChanges: (ProfileState) -> Unit\n    val acceptProfileImageChanges: (ProfileImageState) -> Unit\n    val acceptProfileLinkAction: (ProfileLinkAction) -> Unit\n\n    private val acceptLoadingState: (LoadingState) -> Unit\n\n    private val searchProvider: SearchProvider\n    private val connectionHandler = ConnectionHandler()\n    private val _searchBoxConnectorFlow = MutableSharedFlow<SearchBoxConnector<ResponseSearch>>(1)\n    val searchBoxConnectorFlow\n        get() = _searchBoxConnectorFlow.asSharedFlow()\n\n    var currentImagePickerType: ImagePickerType? = null\n\n    init {\n        val user = userRepository.localUser.value\n\n        val initialProfileState = ProfileState(\n            location = user?.location ?: \"\",\n            birthday = user?.birthday ?: \"\",\n            gender = user?.getGenderWithoutCustomGender() ?: \"\",\n            customGender = user?.getCustomGenderOrNull() ?: \"\",\n            waifuOrHusbando = user?.waifuOrHusbando ?: \"\",\n            character = user?.waifu?.toCharacter(),\n            about = user?.about ?: \"\"\n        )\n\n        val initialProfileImageState = ProfileImageState(\n            currentAvatarUrl = user?.avatar?.originalOrDown(),\n            currentCoverUrl = user?.coverImage?.originalOrDown()\n        )\n\n        val _profileStateFlow = MutableSharedFlow<ProfileState>()\n        profileStateFlow = _profileStateFlow\n            .distinctUntilChanged()\n            .stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.Lazily,\n                initialValue = initialProfileState\n            )\n\n        val _profileImageStateFlow = MutableSharedFlow<ProfileImageState>()\n        profileImageStateFlow = _profileImageStateFlow\n            .distinctUntilChanged()\n            .stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.Lazily,\n                initialValue = initialProfileImageState\n            )\n\n        val _profileLinkEntriesStateFlow = MutableSharedFlow<List<ProfileLinkEntry>>()\n        profileLinkEntriesFlow = _profileLinkEntriesStateFlow\n            .distinctUntilChanged()\n            .stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.Lazily,\n                initialValue = emptyList()\n            )\n\n        acceptProfileChanges = { changes ->\n            viewModelScope.launch { _profileStateFlow.emit(changes) }\n        }\n\n        acceptProfileImageChanges = { changes ->\n            viewModelScope.launch { _profileImageStateFlow.emit(changes) }\n        }\n\n        acceptProfileLinkAction = { action ->\n            val updatedProfileLinkEntries = when (action) {\n                is ProfileLinkAction.Edit -> {\n                    val initialEntry = initialProfileLinksFlow.replayCache.firstOrNull()\n                        ?.firstOrNull { it.site.id == action.profileLinkEntry.site.id }\n\n                    val entry = initialEntry?.copy(url = action.profileLinkEntry.url)\n                        ?: action.profileLinkEntry\n                    profileLinkEntries.filter { it.site != entry.site } + entry\n                }\n\n                is ProfileLinkAction.Delete -> profileLinkEntries.filter { it.site != action.profileLinkEntry.site }\n            }\n\n            viewModelScope.launch {\n                _profileLinkEntriesStateFlow.emit(updatedProfileLinkEntries)\n            }\n        }\n\n        canUpdateProfileFlow = combine(\n            profileStateFlow,\n            profileImageStateFlow,\n            profileLinkEntriesFlow,\n            initialProfileLinksFlow\n        ) { profileState, profileImageState, profileLinkEntries, initialProfileLinkEntries ->\n            profileState != initialProfileState\n                    || profileImageState != initialProfileImageState\n                    || profileLinkEntries.toSet() != initialProfileLinkEntries.toSet()\n        }\n\n        val _loadingStateFlow = MutableSharedFlow<LoadingState>()\n        loadingStateFlow = _loadingStateFlow\n            .distinctUntilChanged()\n            .stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.Lazily,\n                initialValue = LoadingState.NotLoading\n            )\n        acceptLoadingState = { loadingState ->\n            viewModelScope.launch { _loadingStateFlow.emit(loadingState) }\n        }\n\n        searchProvider = SearchProvider(algoliaKeyRepository)\n\n        user?.id?.let { initProfileLinks(it) }\n\n        viewModelScope.launch {\n            val initialProfileLinks = initialProfileLinksFlow.first()\n            _profileLinkEntriesStateFlow.emit(initialProfileLinks + profileLinkEntries)\n        }\n    }\n\n    fun hasUser() = userRepository.hasLocalUser()\n\n    fun updateUserProfile(profileImages: ProfileImageContainer?) {\n        val user = userRepository.localUser.value ?: return\n        val changes = profileState\n        val waifu = if (changes.character != null && changes.waifuOrHusbando.isNotBlank()) {\n            LocalCharacter.empty(changes.character.id)\n        } else {\n            null\n        }\n\n        val updatedUserModel = LocalUser.empty(user.id).copy(\n            location = changes.location,\n            birthday = changes.birthday,\n            gender = if (changes.gender == \"custom\") changes.customGender else changes.gender,\n            waifuOrHusbando = changes.waifuOrHusbando,\n            about = changes.about,\n            waifu = waifu\n        )\n\n        acceptLoadingState(LoadingState.Loading)\n        viewModelScope.launch(Dispatchers.IO) {\n            try {\n                if (user.waifu != null && waifu == null) {\n                    deleteWaifuRelationship(user.id)\n                }\n\n                if (profileImages != null) {\n                    uploadUserImages(user.id, profileImages)\n                }\n\n                updateProfileLinks(user.id)\n\n                val response = userRepository.updateUser(user.id, updatedUserModel)\n\n                if (response != null) {\n                    // request full user model to update local cached model\n                    userRepository.fetchAndStoreLocalUserFromNetwork()\n                    acceptLoadingState(LoadingState.Success)\n                } else {\n                    throw ProfileUpdateException.ProfileDataError(ProfileDataErrorType.UpdateProfile)\n                }\n            } catch (e: Exception) {\n                logE(\"Failed to update user profile.\", e)\n                acceptLoadingState(LoadingState.Error(e))\n            }\n        }\n    }\n\n    private suspend fun deleteWaifuRelationship(userId: String) {\n        logD(\"Deleting waifu relationship.\")\n        val isSuccessful = userRepository.deleteWaifuRelationship(userId)\n        if (!isSuccessful) {\n            throw ProfileUpdateException.ProfileDataError(ProfileDataErrorType.DeleteWaifu)\n        }\n    }\n\n    private suspend fun uploadUserImages(useId: String, profileImages: ProfileImageContainer) {\n        logD(\"Updating user image(s).\")\n        val isSuccessful = userRepository.updateUserImage(useId, profileImages.avatar, profileImages.coverImage)\n        if (!isSuccessful) {\n            throw ProfileUpdateException.ProfileImageError()\n        }\n    }\n\n    private suspend fun updateProfileLinks(userId: String) = withContext(Dispatchers.IO) {\n        val initialProfileLinks = initialProfileLinksFlow.replayCache.firstOrNull() ?: emptyList()\n\n        val newProfileLinks = profileLinkEntries.filter { newEntry ->\n            initialProfileLinks.none { it.site.id == newEntry.site.id }\n        }\n\n        val updatedProfileLinks = profileLinkEntries.filter { newEntry ->\n            newEntry.id != null && initialProfileLinks.none { it == newEntry }\n        }\n\n        val deletedProfileLinks = initialProfileLinks.filter { initialEntry ->\n            initialEntry.id != null && profileLinkEntries.none { it.site.id == initialEntry.site.id }\n        }\n\n        newProfileLinks.forEach { profileLinkEntry ->\n            try {\n                val createdProfileLink = createProfileLink(profileLinkEntry, userId)\n                    ?: throw NoDataException(\"Received response is null.\")\n                addOrUpdateInitialProfileLink(profileLinkEntry, createdProfileLink)\n            } catch (e: Exception) {\n                logE(\"Failed to create profile link $profileLinkEntry.\", e)\n                throw ProfileUpdateException.ProfileLinkError(\n                    ProfileLinkOperation.Create,\n                    profileLinkEntry\n                )\n            }\n        }\n\n        updatedProfileLinks.forEach { profileLinkEntry ->\n            try {\n                val updatedProfileLink = updateProfileLink(profileLinkEntry, userId)\n                    ?: throw NoDataException(\"Received response is null.\")\n                addOrUpdateInitialProfileLink(profileLinkEntry, updatedProfileLink)\n            } catch (e: Exception) {\n                logE(\"Failed to update profile link $profileLinkEntry.\", e)\n                throw ProfileUpdateException.ProfileLinkError(\n                    ProfileLinkOperation.Update,\n                    profileLinkEntry\n                )\n            }\n        }\n\n        deletedProfileLinks.forEach { profileLinkEntry ->\n            try {\n                val isSuccessful = profileLinkRepository.deleteProfileLink(profileLinkEntry.id!!)\n                if (!isSuccessful) {\n                    throw NoDataException(\"Failed to delete profile link.\")\n                }\n                removeFromInitialProfileLinks(profileLinkEntry)\n            } catch (e: Exception) {\n                logE(\"Failed to delete profile link $profileLinkEntry.\", e)\n                throw ProfileUpdateException.ProfileLinkError(\n                    ProfileLinkOperation.Delete,\n                    profileLinkEntry\n                )\n            }\n        }\n    }\n\n    private suspend fun createProfileLink(\n        profileLinkEntry: ProfileLinkEntry,\n        userId: String\n    ): ProfileLink? {\n        return profileLinkRepository.createProfileLink(\n            userId,\n            profileLinkEntry.site.id,\n            profileLinkEntry.url\n        )\n    }\n\n    private suspend fun updateProfileLink(\n        profileLinkEntry: ProfileLinkEntry,\n        userId: String\n    ): ProfileLink? {\n        return profileLinkRepository.updateProfileLink(\n            userId,\n            profileLinkEntry.id!!,\n            profileLinkEntry.url\n        )\n    }\n\n    private fun addOrUpdateInitialProfileLink(\n        localProfileLinkEntry: ProfileLinkEntry,\n        remoteProfileLink: ProfileLink\n    ) {\n        val initialProfileLinkEntry = initialProfileLinksFlow.replayCache.firstOrNull()\n            ?.find { it.id == remoteProfileLink.id || it.site.id == remoteProfileLink.profileLinkSite?.id }\n\n        val updatedProfileLinkEntry = when (initialProfileLinkEntry) {\n            // add new profile link entry to initialProfileLinks\n            null -> localProfileLinkEntry.copy(\n                id = remoteProfileLink.id,\n                url = remoteProfileLink.url ?: localProfileLinkEntry.url,\n                site = remoteProfileLink.profileLinkSite ?: localProfileLinkEntry.site\n            )\n\n            // update initialProfileLinkEntry with remoteProfileLink\n            else -> initialProfileLinkEntry.copy(\n                id = remoteProfileLink.id,\n                url = remoteProfileLink.url ?: localProfileLinkEntry.url\n            )\n        }\n        val initialProfileLinksWithoutEntry = getInitialProfileLinksWithout(remoteProfileLink)\n\n        viewModelScope.launch {\n            initialProfileLinksFlow.emit(initialProfileLinksWithoutEntry + updatedProfileLinkEntry)\n        }\n    }\n\n    private fun removeFromInitialProfileLinks(profileLinkEntry: ProfileLinkEntry) {\n        val initialProfileLinksWithoutEntry = getInitialProfileLinksWithout(\n            ProfileLink(\n                id = profileLinkEntry.id ?: \"\",\n                url = profileLinkEntry.url,\n                profileLinkSite = profileLinkEntry.site,\n                user = null\n            )\n        )\n        viewModelScope.launch {\n            initialProfileLinksFlow.emit(initialProfileLinksWithoutEntry)\n        }\n    }\n\n    private fun getInitialProfileLinksWithout(profileLink: ProfileLink): List<ProfileLinkEntry> {\n        return initialProfileLinksFlow.replayCache.firstOrNull()\n            ?.filterNot { it.id == profileLink.id || it.site.id == profileLink.profileLinkSite?.id }\n            ?: emptyList()\n    }\n\n    fun initSearchClient() {\n        if (searchProvider.isInitialized) return\n        val characterSearchQuery = query {\n            attributesToRetrieve {\n                +\"id\"\n                +\"slug\"\n                +\"canonicalName\"\n                +\"image\"\n                +\"primaryMedia\"\n            }\n            responseFields {\n                +Hits\n            }\n            attributesToHighlight { }\n            removeStopWords = RemoveStopWords.False\n            removeWordsIfNoResults = RemoveWordIfNoResults.AllOptional\n        }\n        createSearchClient(characterSearchQuery)\n    }\n\n    private fun createSearchClient(query: Query) {\n        viewModelScope.launch(Dispatchers.IO) {\n            try {\n                searchProvider.createSearchClient(\n                    SearchType.Characters,\n                    query,\n                    triggerSearchFor = { !it.query.isNullOrBlank() }\n                ) { searcher ->\n                    connectionHandler.clear()\n                    val searchBoxConnector = SearchBoxConnector(searcher)\n                    connectionHandler += searchBoxConnector\n                    _searchBoxConnectorFlow.emit(searchBoxConnector)\n                }\n            } catch (e: Exception) {\n                logE(\"Failed to create search client.\", e)\n            }\n        }\n    }\n\n    private fun initProfileLinks(userId: String) {\n        viewModelScope.launch(Dispatchers.IO) {\n            acceptLoadingState(LoadingState.Loading)\n            val userProfileLinks = fetchUserProfileLinks(userId).mapNotNull { profileLink ->\n                val url = profileLink.url ?: return@mapNotNull null\n                val site = profileLink.profileLinkSite ?: return@mapNotNull null\n                ProfileLinkEntry(profileLink.id, url, site)\n            }\n            initialProfileLinksFlow.emit(userProfileLinks)\n            acceptLoadingState(LoadingState.NotLoading)\n        }\n    }\n\n    private suspend fun fetchUserProfileLinks(userId: String): List<ProfileLink> {\n        return try {\n            val filter = Filter()\n                .limit(50)\n                .include(\"profileLinkSite\")\n            userRepository.getProfileLinksForUser(userId, filter) ?: emptyList()\n        } catch (e: Exception) {\n            logE(\"Failed to fetch profile links for user.\", e)\n            emptyList()\n        }\n    }\n\n    fun loadProfileLinkSites() {\n        if (!profileLinkSites.isNullOrEmpty()) return\n        viewModelScope.launch(Dispatchers.IO) {\n            _profileLinkSitesLoadStateFlow.emit(true)\n            val profileLinkSites = fetchProfileLinkSites()\n            _profileLinkSitesFlow.emit(profileLinkSites)\n            _profileLinkSitesLoadStateFlow.emit(false)\n        }\n    }\n\n    private suspend fun fetchProfileLinkSites(): List<ProfileLinkSite> {\n        return try {\n            val filter = Filter().pageLimit(50)\n            profileLinkRepository.getAllProfileLinkSites(filter) ?: emptyList()\n        } catch (e: Exception) {\n            logE(\"Failed to fetch profile link sites.\", e)\n            emptyList()\n        }\n    }\n\n    private fun LocalUser.getGenderWithoutCustomGender(): String? {\n        return when (gender) {\n            null, \"\", \"male\", \"female\", \"secret\" -> gender\n            else -> \"custom\"\n        }\n    }\n\n    private fun LocalUser.getCustomGenderOrNull(): String? {\n        return when (gender) {\n            \"male\", \"female\", \"secret\" -> null\n            else -> gender\n        }\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        connectionHandler.clear()\n        searchProvider.cancel()\n    }\n}\n\ndata class ProfileState(\n    val location: String,\n    val birthday: String,\n    val gender: String,\n    val customGender: String,\n    val waifuOrHusbando: String,\n    val character: Character?,\n    val about: String\n)\n\ndata class ProfileImageState(\n    val currentAvatarUrl: String?,\n    val currentCoverUrl: String?,\n    val selectedAvatarUri: Uri? = null,\n    val selectedCoverUri: Uri? = null\n)\n\ndata class ProfileImageContainer(\n    val avatar: String?,\n    val coverImage: String?\n)\n\n@Parcelize\ndata class ProfileLinkEntry(\n    val id: String? = null,\n    val url: String,\n    val site: ProfileLinkSite\n) : Parcelable\n\nsealed class ProfileLinkAction {\n    data class Edit(val profileLinkEntry: ProfileLinkEntry) : ProfileLinkAction()\n    data class Delete(val profileLinkEntry: ProfileLinkEntry) : ProfileLinkAction()\n}\n\nsealed class LoadingState {\n    data object NotLoading : LoadingState()\n    data object Loading : LoadingState()\n    data object Success : LoadingState()\n    data class Error(val exception: Exception, var isConsumed: Boolean = false) : LoadingState()\n}\n\nsealed class ProfileUpdateException : Exception() {\n    class ProfileDataError(val type: ProfileDataErrorType) : ProfileUpdateException() {\n        override val message: String\n            get() = \"Failed to update profile data. Type: $type\"\n    }\n\n    class ProfileImageError : ProfileUpdateException()\n\n    class ProfileLinkError(\n        val operation: ProfileLinkOperation,\n        val profileLinkEntry: ProfileLinkEntry\n    ) : ProfileUpdateException() {\n        override val message: String\n            get() = \"Failed to $operation profile link $profileLinkEntry.\"\n    }\n}\n\nenum class ProfileDataErrorType {\n    UpdateProfile, DeleteWaifu\n}\n\nenum class ProfileLinkOperation {\n    Create, Update, Delete\n}\n\nenum class ImagePickerType {\n    AVATAR, COVER\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/profile/editprofile/SelectProfileLinkSiteBottomSheet.kt",
    "content": "package io.github.drumber.kitsune.ui.profile.editprofile\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.content.res.ResourcesCompat\nimport androidx.core.os.bundleOf\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.setFragmentResult\nimport androidx.lifecycle.lifecycleScope\nimport com.google.android.material.bottomsheet.BottomSheetDialogFragment\nimport io.github.drumber.kitsune.data.presentation.model.user.profilelinks.ProfileLinkSite\nimport io.github.drumber.kitsune.databinding.ItemListOptionBinding\nimport io.github.drumber.kitsune.databinding.SheetSelectProfileLinkSiteBinding\nimport io.github.drumber.kitsune.util.ItemClickListener\nimport io.github.drumber.kitsune.util.ui.getProfileSiteLogoResourceId\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass SelectProfileLinkSiteBottomSheet : BottomSheetDialogFragment() {\n\n    private val viewModel: EditProfileViewModel by viewModel(ownerProducer = { requireParentFragment() })\n\n    private var _binding: SheetSelectProfileLinkSiteBinding? = null\n    private val binding get() = _binding!!\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = SheetSelectProfileLinkSiteBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        viewModel.loadProfileLinkSites()\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.profileLinkSitesFlow.collectLatest { profileLinkSites ->\n                val addedProfileLinks = viewModel.profileLinkEntries\n                val profileLinksWithoutAddedEntries = profileLinkSites.filter { profileLinkSite ->\n                    addedProfileLinks.none { it.site.id == profileLinkSite.id }\n                }\n                updateProfileLinkSites(profileLinksWithoutAddedEntries)\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewModel.profileLinkSitesLoadStateFlow.collectLatest {\n                binding.progressBarProfileLinkSites.isVisible = it\n            }\n        }\n    }\n\n    private fun updateProfileLinkSites(linkSites: List<ProfileLinkSite>) {\n        binding.layoutListParent.removeAllViews()\n        linkSites.forEach { linkSite ->\n            if (linkSite.name.isNullOrBlank()) return@forEach\n            val itemBinding =\n                ItemListOptionBinding.inflate(layoutInflater, binding.layoutListParent, true)\n            itemBinding.title = linkSite.name\n            itemBinding.icon = ResourcesCompat.getDrawable(\n                resources,\n                getProfileSiteLogoResourceId(linkSite.name),\n                activity?.theme\n            )\n            itemBinding.listener = ItemClickListener { onItemClicked(linkSite) }\n        }\n    }\n\n    private fun onItemClicked(linkSite: ProfileLinkSite) {\n        setFragmentResult(\n            PROFILE_SITE_SELECTED_REQUEST_KEY,\n            bundleOf(BUNDLE_PROFILE_LINK_SITE to linkSite)\n        )\n        dismiss()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        _binding = null\n    }\n\n    companion object {\n        const val TAG = \"select_profile_link_site_bottom_sheet\"\n        const val BUNDLE_PROFILE_LINK_SITE = \"profile_link_site_bundle_key\"\n        const val PROFILE_SITE_SELECTED_REQUEST_KEY = \"site_selected_request_key\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/search/SearchFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.search\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.inputmethod.InputMethodManager\nimport androidx.core.view.doOnPreDraw\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport androidx.navigation.fragment.FragmentNavigatorExtras\nimport androidx.navigation.fragment.findNavController\nimport androidx.paging.LoadState\nimport com.algolia.instantsearch.android.searchbox.SearchBoxViewAppCompat\nimport com.algolia.instantsearch.core.connection.AbstractConnection\nimport com.algolia.instantsearch.core.connection.ConnectionHandler\nimport com.algolia.instantsearch.searchbox.SearchBoxConnector\nimport com.algolia.instantsearch.searchbox.connectView\nimport com.algolia.search.model.response.ResponseSearch\nimport com.bumptech.glide.Glide\nimport com.google.android.material.badge.BadgeDrawable\nimport com.google.android.material.badge.BadgeUtils\nimport com.google.android.material.navigation.NavigationBarView\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaDto\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.databinding.FragmentSearchBinding\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.adapter.OnItemClickListener\nimport io.github.drumber.kitsune.ui.adapter.paging.MediaSearchPagingAdapter\nimport io.github.drumber.kitsune.ui.adapter.paging.ResourceLoadStateAdapter\nimport io.github.drumber.kitsune.ui.component.LoadStateSpanSizeLookup\nimport io.github.drumber.kitsune.ui.component.ResponsiveGridLayoutManager\nimport io.github.drumber.kitsune.ui.component.updateLoadState\nimport io.github.drumber.kitsune.ui.main.FragmentDecorationPreference\nimport io.github.drumber.kitsune.ui.search.SearchViewModel.SearchClientStatus.Error\nimport io.github.drumber.kitsune.ui.search.SearchViewModel.SearchClientStatus.Initialized\nimport io.github.drumber.kitsune.ui.search.SearchViewModel.SearchClientStatus.NotAvailable\nimport io.github.drumber.kitsune.ui.search.SearchViewModel.SearchClientStatus.NotInitialized\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.activityViewModel\nimport java.lang.ref.WeakReference\n\nclass SearchFragment : Fragment(R.layout.fragment_search),\n    FragmentDecorationPreference,\n    OnItemClickListener<Media>,\n    NavigationBarView.OnItemReselectedListener {\n\n    override val hasTransparentStatusBar = false\n\n    private var _binding: FragmentSearchBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: SearchViewModel by activityViewModel()\n\n    private val connectionHandler = ConnectionHandler()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentSearchBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        postponeEnterTransition()\n\n        if (findNavController().currentBackStackEntry?.arguments == null) {\n            view.doOnPreDraw { startPostponedEnterTransition() }\n        } else {\n            // safeguard: ensure startPostponedEnterTransition() got called within 200ms\n            view.postDelayed({\n                startPostponedEnterTransition()\n            }, 200)\n        }\n\n        binding.apply {\n            root.initPaddingWindowInsetsListener(\n                left = true,\n                top = true,\n                right = true,\n                consume = false\n            )\n            rvMedia.initPaddingWindowInsetsListener(bottom = true, consume = false)\n        }\n\n        initRecyclerView()\n        initSearchBar()\n        observeSearchBox()\n        observeFilters()\n        initSearchProviderStatusLayout()\n    }\n\n    private fun initRecyclerView() {\n        val adapter = MediaSearchPagingAdapter(Glide.with(this), this)\n        val columnWidth = resources.getDimension(KitsunePref.mediaItemSize.widthRes) +\n                2 * resources.getDimension(R.dimen.media_item_margin)\n        val gridLayout = ResponsiveGridLayoutManager(requireContext(), columnWidth.toInt(), 2)\n        gridLayout.spanSizeLookup = LoadStateSpanSizeLookup(adapter, gridLayout)\n\n        binding.rvMedia.adapter = adapter.withLoadStateHeaderAndFooter(\n            header = ResourceLoadStateAdapter(adapter),\n            footer = ResourceLoadStateAdapter(adapter)\n        )\n        binding.rvMedia.layoutManager = gridLayout\n        binding.rvMedia.itemAnimator = null\n\n        binding.layoutLoading.btnRetry.setOnClickListener { adapter.retry() }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                adapter.loadStateFlow.collectLatest { loadState ->\n                    binding.layoutLoading.updateLoadState(\n                        binding.rvMedia,\n                        adapter.itemCount,\n                        loadState\n                    )\n\n                    if (loadState.refresh is LoadState.NotLoading) {\n                        binding.rvMedia.doOnPreDraw { startPostponedEnterTransition() }\n                    }\n                }\n            }\n        }\n\n        viewLifecycleOwner.lifecycleScope.launch {\n            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                viewModel.searchResultSource.collectLatest {\n                    adapter.submitData(it)\n                }\n            }\n        }\n    }\n\n    private fun initSearchBar() {\n        binding.btnSearch.setOnClickListener {\n            val isSearchFocussed = binding.searchView.getTag(TAG_SEARCH_FOCUSED) as? Boolean\n            if (isSearchFocussed == true) {\n                val focusedView = binding.searchView.findFocus()\n                focusedView.clearFocus()\n                val imm =\n                    requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager\n                imm.hideSoftInputFromWindow(focusedView.windowToken, 0)\n            } else {\n                focusSearchView()\n            }\n        }\n\n        binding.searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->\n            binding.btnSearch.setImageResource(\n                if (hasFocus) R.drawable.ic_arrow_back_24 else R.drawable.ic_search_24\n            )\n            binding.searchView.setTag(TAG_SEARCH_FOCUSED, hasFocus)\n        }\n\n        binding.btnFilter.apply {\n            setOnClickListener {\n                val action = SearchFragmentDirections.actionSearchFragmentToFacetFragment()\n                findNavController().navigateSafe(R.id.search_fragment, action)\n            }\n            setOnLongClickListener {\n                if (!viewModel.filtersLiveData.value?.getFilters().isNullOrEmpty()) {\n                    viewModel.clearSearchFilter()\n                    return@setOnLongClickListener true\n                }\n                false\n            }\n        }\n    }\n\n    private fun observeSearchBox() {\n        viewModel.searchBox.observe(viewLifecycleOwner) { searchBox ->\n            val searchBoxView = SearchBoxViewAppCompat(binding.searchView)\n            connectionHandler += searchBox.connectView(searchBoxView)\n            connectionHandler += SearchResponseListener(searchBox) {\n                binding.rvMedia.post {\n                    if (!isAdded) return@post\n                    // scroll to top when searching\n                    binding.rvMedia.scrollToPosition(0)\n                    binding.appBarLayout.setExpanded(true)\n                }\n            }\n        }\n    }\n\n    private fun initSearchProviderStatusLayout() {\n        binding.layoutSearchProviderStatus.btnRetrySearchProvider.setOnClickListener {\n            viewModel.initializeSearchClient()\n        }\n\n        viewModel.searchClientStatus.observe(viewLifecycleOwner) { status ->\n            binding.layoutSearchProviderStatus.apply {\n                root.isVisible = status != Initialized\n                btnRetrySearchProvider.isVisible = status == Error || status == NotAvailable\n                tvStatus.isVisible = btnRetrySearchProvider.isVisible\n                progressBarSearchProvider.isVisible = status == NotInitialized\n            }\n        }\n    }\n\n    @SuppressLint(\"UnsafeOptInUsageError\")\n    private fun observeFilters() {\n        viewModel.filtersLiveData.observe(viewLifecycleOwner) { filters ->\n            val filterCount = filters?.getFilters()?.size ?: 0\n            binding.btnFilter.post {\n                if (!isAdded) return@post\n                binding.btnFilter.overlay.clear()\n                val badgeDrawable = BadgeDrawable.create(binding.btnFilter.context).apply {\n                    isVisible = filterCount > 0\n                    number = filterCount\n                }\n                BadgeUtils.attachBadgeDrawable(badgeDrawable, binding.btnFilter)\n            }\n        }\n    }\n\n    override fun onItemClick(view: View, item: Media) {\n        val action = SearchFragmentDirections.actionSearchFragmentToDetailsFragment(item.toMediaDto())\n        val detailsTransitionName = getString(R.string.details_poster_transition_name)\n        val extras = FragmentNavigatorExtras(view to detailsTransitionName)\n        findNavController().navigateSafe(R.id.search_fragment, action, extras)\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        binding.appBarLayout.setExpanded(true)\n        if (binding.rvMedia.canScrollVertically(-1)) {\n            binding.rvMedia.smoothScrollToPosition(0)\n        } else {\n            focusSearchView()\n        }\n    }\n\n    private fun focusSearchView() {\n        binding.searchView.requestFocus()\n        val imm =\n            requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager\n        imm.showSoftInput(binding.searchView.findFocus(), InputMethodManager.SHOW_IMPLICIT)\n    }\n\n    override fun onDestroyView() {\n        connectionHandler.clear()\n        super.onDestroyView()\n        _binding = null\n    }\n\n    companion object {\n        @SuppressLint(\"NonConstantResourceId\")\n        const val TAG_SEARCH_FOCUSED = R.drawable.ic_search_24\n    }\n\n    /**\n     * Triggers the onSearchReceived callback after the\n     * search query was changed AND the response is received.\n     */\n    private class SearchResponseListener(\n        searchBox: SearchBoxConnector<ResponseSearch>,\n        private val onSearchReceived: () -> Unit\n    ) : AbstractConnection() {\n\n        private val _searchBox = WeakReference(searchBox)\n        private var pendingSearch = false\n\n        private val onQueryChanged = { _: Any? ->\n            pendingSearch = true\n        }\n        private val onSearchResponse = { r: ResponseSearch? ->\n            // new data was received while there is a pending search, so notify the callback\n            if (pendingSearch) {\n                onSearchReceived()\n            }\n            // reset pendingSearch flag when the first page was received\n            if (pendingSearch && r?.pageOrNull == 0) {\n                pendingSearch = false\n            }\n        }\n\n        override fun connect() {\n            super.connect()\n            _searchBox.get()?.let {\n                it.viewModel.query.subscribe(onQueryChanged)\n                it.searcher.response.subscribe(onSearchResponse)\n            }\n        }\n\n        override fun disconnect() {\n            super.disconnect()\n            _searchBox.get()?.let {\n                it.viewModel.query.unsubscribe(onQueryChanged)\n                it.searcher.response.unsubscribe(onSearchResponse)\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/search/SearchViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.search\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.asFlow\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.PagingConfig\nimport androidx.paging.cachedIn\nimport com.algolia.instantsearch.android.paging3.Paginator\nimport com.algolia.instantsearch.android.paging3.filterstate.connectPaginator\nimport com.algolia.instantsearch.android.paging3.flow\nimport com.algolia.instantsearch.android.paging3.searchbox.connectPaginator\nimport com.algolia.instantsearch.core.connection.AbstractConnection\nimport com.algolia.instantsearch.core.connection.ConnectionHandler\nimport com.algolia.instantsearch.core.selectable.list.SelectionMode\nimport com.algolia.instantsearch.filter.facet.DefaultFacetListPresenter\nimport com.algolia.instantsearch.filter.facet.FacetListConnector\nimport com.algolia.instantsearch.filter.facet.FacetSortCriterion\nimport com.algolia.instantsearch.filter.state.FilterState\nimport com.algolia.instantsearch.filter.state.Filters\nimport com.algolia.instantsearch.filter.state.groupOr\nimport com.algolia.instantsearch.searchbox.SearchBoxConnector\nimport com.algolia.instantsearch.searcher.connectFilterState\nimport com.algolia.instantsearch.searcher.hits.HitsSearcher\nimport com.algolia.search.dsl.attributesToRetrieve\nimport com.algolia.search.dsl.query\nimport com.algolia.search.model.Attribute\nimport com.algolia.search.model.filter.Filter\nimport com.algolia.search.model.response.ResponseSearch\nimport com.algolia.search.model.search.Query\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.constants.Repository\nimport io.github.drumber.kitsune.data.mapper.AlgoliaMapper.toMedia\nimport io.github.drumber.kitsune.data.presentation.model.algolia.SearchType\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.repository.AlgoliaKeyRepository\nimport io.github.drumber.kitsune.data.source.network.algolia.model.search.AlgoliaMediaSearchResult\nimport io.github.drumber.kitsune.domain.algolia.FilterCollection\nimport io.github.drumber.kitsune.domain.algolia.SearchProvider\nimport io.github.drumber.kitsune.domain.algolia.toCombinedMap\nimport io.github.drumber.kitsune.domain.algolia.toFilterCollection\nimport io.github.drumber.kitsune.data.common.exception.SearchProviderUnavailableException\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.component.algolia.SeasonListPresenter\nimport io.github.drumber.kitsune.ui.component.algolia.range.CustomFilterRangeConnector\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.logI\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport java.util.Calendar\n\nclass SearchViewModel(\n    algoliaKeyRepository: AlgoliaKeyRepository\n) : ViewModel() {\n\n    private val searchProvider = SearchProvider(algoliaKeyRepository)\n\n    private val searchSelector = MutableLiveData<Pair<SearchType, HitsSearcher>>()\n\n    private var filterState: FilterState? = null\n\n    private val searchPaginator = MutableLiveData<Paginator<Media>>()\n\n    private val _filtersLiveData = MutableLiveData<Filters?>()\n    val filtersLiveData get() = _filtersLiveData as LiveData<Filters?>\n\n    private val _searchClientStatus = MutableLiveData(SearchClientStatus.NotInitialized)\n    val searchClientStatus get() = _searchClientStatus as LiveData<SearchClientStatus>\n\n    private val _searchBox = MutableLiveData<SearchBoxConnector<ResponseSearch>>()\n    val searchBox get() = _searchBox as LiveData<SearchBoxConnector<ResponseSearch>>\n\n    private val _filterFacets = MutableLiveData<FilterFacets>()\n    val filterFacets get() = _filterFacets as LiveData<FilterFacets>\n\n    private val connectionHandler = ConnectionHandler()\n\n    private val json = Json { ignoreUnknownKeys = true }\n\n    init {\n        initializeSearchClient()\n    }\n\n    fun initializeSearchClient() {\n        if (searchProvider.isInitialized) return\n        val query = query {\n            attributesToRetrieve {\n                +\"id\"\n                +\"slug\"\n                +\"kind\"\n                +\"canonicalTitle\"\n                +\"titles\"\n                +\"posterImage\"\n                +\"subtype\"\n            }\n        }\n        createSearchClient(SearchType.Media, query)\n    }\n\n    private fun createSearchClient(searchType: SearchType, query: Query) {\n        viewModelScope.launch {\n            filterState = null\n            _searchClientStatus.postValue(SearchClientStatus.NotInitialized)\n            try {\n                searchProvider.createSearchClient(searchType, query) { searcher ->\n                    searchPaginator.value?.invalidate()\n                    connectionHandler.clear()\n                    searchSelector.postValue(Pair(searchType, searcher))\n\n                    val paginator = Paginator(\n                        searcher = searcher,\n                        pagingConfig = PagingConfig(\n                            pageSize = Kitsu.DEFAULT_PAGE_SIZE,\n                            maxSize = Repository.MAX_CACHED_ITEMS\n                        ),\n                        transformer = { hit ->\n                            when (searchType) {\n                                SearchType.Media -> json.decodeFromJsonElement<AlgoliaMediaSearchResult>(hit.json).toMedia()\n                                else -> throw IllegalStateException(\"Search type '$searchType' is not supported.\")\n                            }\n                        }\n                    )\n                    searchPaginator.postValue(paginator)\n\n                    val filterState = if (KitsunePref.rememberSearchFilters) {\n                        val storedFilters = KitsunePref.searchFilters.toCombinedMap()\n                        FilterState(storedFilters)\n                    } else {\n                        FilterState()\n                    }\n                    createFilterFacets(searcher, filterState)\n                    connectionHandler += searcher.connectFilterState(filterState)\n                    connectionHandler += filterState.connectPaginator(paginator)\n\n                    _filtersLiveData.postValue(filterState.filters.value)\n                    filterState.filters.subscribe {\n                        _filtersLiveData.postValue(it)\n                        // store search filters\n                        KitsunePref.searchFilters = it.toFilterCollection()\n                    }\n\n                    createSearchBox(searcher, paginator)\n                    this@SearchViewModel.filterState = filterState\n                    _searchClientStatus.postValue(SearchClientStatus.Initialized)\n                }\n            } catch (e: SearchProviderUnavailableException) {\n                logI(\"Search provider not available. Is the device offline?\")\n                _searchClientStatus.postValue(SearchClientStatus.NotAvailable)\n            } catch (e: Exception) {\n                logE(\"Could not create search client.\", e)\n                _searchClientStatus.postValue(SearchClientStatus.Error)\n            }\n        }\n    }\n\n    private fun createSearchBox(searcher: HitsSearcher, paginator: Paginator<Media>) {\n        val searchBox = SearchBoxConnector(searcher)\n        connectionHandler += searchBox\n        connectionHandler += searchBox.connectPaginator(paginator)\n        _searchBox.postValue(searchBox)\n    }\n\n    val searchResultSource = searchPaginator.asFlow().flatMapLatest { paginator ->\n        paginator.flow\n    }.cachedIn(viewModelScope)\n\n\n    private fun createFilterFacets(searcher: HitsSearcher, filterState: FilterState) {\n        val filterFacets = FilterFacets(searcher, filterState)\n        applyCategoryFilters(filterState)\n        _filterFacets.postValue(filterFacets)\n    }\n\n    fun clearSearchFilter() {\n        filterState?.notify {\n            clear(*getGroups().keys.toTypedArray())\n        }\n        KitsunePref.searchFilters = FilterCollection()\n        KitsunePref.searchCategories = emptyList()\n        _filtersLiveData.postValue(null)\n    }\n\n    private fun applyCategoryFilters(filterState: FilterState) {\n        filterState.notify {\n            val categories = Attribute(\"categories\")\n            val filterFacets = KitsunePref.searchCategories.mapNotNull { wrapper ->\n                wrapper.categoryName?.let { categoryName ->\n                    Filter.Facet(categories, categoryName)\n                }\n            }\n\n            val group = groupOr(categories)\n            clear(group)\n            if (filterFacets.isNotEmpty()) {\n                add(group, *filterFacets.toTypedArray())\n            }\n        }\n    }\n\n    fun updateCategoryFilters() {\n        filterState?.let { applyCategoryFilters(it) }\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        searchProvider.cancel()\n        connectionHandler.clear()\n    }\n\n    inner class FilterFacets(\n        searcher: HitsSearcher,\n        filterState: FilterState\n    ) {\n\n        val kindConnector = FacetListConnector(\n            searcher = searcher,\n            filterState = filterState,\n            attribute = Attribute(\"kind\"),\n            selectionMode = SelectionMode.Multiple,\n        ).bind()\n        val kindPresenter = DefaultFacetListPresenter(limit = 2)\n\n        val yearConnector = CustomFilterRangeConnector(\n            filterState = filterState,\n            attribute = Attribute(\"year\"),\n            range = minYear..maxYear,\n            bounds = minYear..maxYear\n        ).bind()\n\n        val avgRatingConnector = CustomFilterRangeConnector(\n            filterState = filterState,\n            attribute = Attribute(\"averageRating\"),\n            range = 5..100,\n            bounds = 5..100\n        ).bind()\n\n        val seasonConnector = FacetListConnector(\n            searcher = searcher,\n            filterState = filterState,\n            attribute = Attribute(\"season\"),\n            selectionMode = SelectionMode.Multiple,\n        ).bind()\n        val seasonPresenter = SeasonListPresenter()\n\n        val subtypeConnector = FacetListConnector(\n            searcher = searcher,\n            filterState = filterState,\n            attribute = Attribute(\"subtype\"),\n            selectionMode = SelectionMode.Multiple,\n        ).bind()\n        val subtypePresenter = DefaultFacetListPresenter(limit = 100, sortBy = defaultFacetSortBy)\n\n        val streamersConnector = FacetListConnector(\n            searcher = searcher,\n            filterState = filterState,\n            attribute = Attribute(\"streamers\"),\n            selectionMode = SelectionMode.Multiple,\n        ).bind()\n        val streamersPresenter = DefaultFacetListPresenter(limit = 100, sortBy = defaultFacetSortBy)\n\n        val ageRatingConnector = FacetListConnector(\n            searcher = searcher,\n            filterState = filterState,\n            attribute = Attribute(\"ageRating\"),\n            selectionMode = SelectionMode.Multiple,\n        ).bind()\n        val ageRatingPresenter = DefaultFacetListPresenter(limit = 4, sortBy = defaultFacetSortBy)\n\n        private fun <T : AbstractConnection> T.bind() = apply { connectionHandler += this }\n    }\n\n    companion object {\n        private val defaultFacetSortBy\n            get() = listOf(\n                FacetSortCriterion.IsRefined,\n                FacetSortCriterion.CountDescending\n            )\n\n        val maxYear get() = Calendar.getInstance().get(Calendar.YEAR) + 2\n        const val minYear = 1862\n    }\n\n    enum class SearchClientStatus {\n        NotInitialized,\n        Initialized,\n        NotAvailable,\n        Error\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/search/categories/CategoriesDialogFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.search.categories\n\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.FragmentManager\nimport com.google.android.material.snackbar.Snackbar\nimport com.unnamed.b.atv.model.TreeNode\nimport com.unnamed.b.atv.view.AndroidTreeView\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.presentation.model.media.category.CategoryNode\nimport io.github.drumber.kitsune.databinding.FragmentCategoriesBinding\nimport io.github.drumber.kitsune.preference.CategoryPrefWrapper\nimport io.github.drumber.kitsune.ui.base.BaseDialogFragment\nimport io.github.drumber.kitsune.util.network.ResponseData\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass CategoriesDialogFragment : BaseDialogFragment(R.layout.fragment_categories) {\n\n    private var _binding: FragmentCategoriesBinding? = null\n    private val binding get() = _binding!!\n\n    private val viewModel: CategoriesViewModel by viewModel()\n\n    private var onDismissListener: DialogInterface.OnDismissListener? = null\n\n    private lateinit var treeView: AndroidTreeView\n    private lateinit var treeRoot: TreeNode\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentCategoriesBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        binding.apply {\n            collapsingToolbar.initWindowInsetsListener(consume = false)\n            toolbar.initWindowInsetsListener(consume = false)\n            toolbar.setNavigationOnClickListener { dismiss() }\n            toolbar.inflateMenu(R.menu.category_dialog_menu)\n            toolbar.setOnMenuItemClickListener { onMenuItemClicked(it) }\n            nestedScrollView.initPaddingWindowInsetsListener(\n                left = true,\n                right = true,\n                bottom = true,\n                consume = false\n            )\n            layoutLoading.btnRetry.setOnClickListener { viewModel.fetchChildCategories(null) }\n        }\n\n        toggleLoadingLayout(true)\n        initTreeView()\n    }\n\n    private fun initTreeView() {\n        treeRoot = TreeNode.root()\n        treeView = AndroidTreeView(requireContext(), treeRoot)\n        treeView.setDefaultAnimation(true)\n        treeView.setDefaultContainerStyle(R.style.TreeNodeStyle)\n        treeView.isSelectionModeEnabled = true\n        var isTreeViewDataSet = false\n\n        viewModel.categoryNodes.observe(viewLifecycleOwner) { response ->\n            toggleLoadingLayout(false)\n            if (response !is ResponseData.Success) {\n                displayLoadingError((response as ResponseData.Error).e)\n                return@observe\n            }\n            val categories = response.data\n\n            if (isTreeViewDataSet) {\n                // restore state after fetching a new category\n                viewModel.treeViewSavedState = treeView.saveState\n            }\n            treeRoot = TreeNode.root()\n\n            categories\n                .sortedBy { it.category.title }\n                .forEach { category ->\n                    addCategoryTreeNode(treeRoot, category)\n                }\n\n            val prevScrollY = binding.nestedScrollView.scrollY\n\n            treeView.setDefaultAnimation(false)\n            treeView.setRoot(treeRoot)\n            binding.treeViewContainer.apply {\n                removeAllViews()\n                addView(treeView.view)\n            }\n            viewModel.treeViewSavedState?.let { treeView.restoreState(it) }\n            viewModel.selectedCategories.toSet().forEach { categoryWrapper ->\n                selectTreeNodeForCategory(treeRoot, categoryWrapper.categoryId)\n            }\n            isTreeViewDataSet = true\n\n            // restore scroll position\n            binding.nestedScrollView.scrollTo(0, prevScrollY)\n            treeView.setDefaultAnimation(true)\n\n            updateSelectionCounter()\n        }\n    }\n\n    private fun addCategoryTreeNode(parent: TreeNode, categoryNode: CategoryNode) {\n        val node = TreeNode(categoryNode)\n        val viewHolder = CategoryViewHolder(requireContext()) {\n            if (it.childCategories.isEmpty()) {\n                viewModel.fetchChildCategories(it)\n            }\n        }\n        viewHolder.onSelectionChangeListener = { onNodeSelectionChange(it) }\n        node.viewHolder = viewHolder\n        node.isSelectable = true\n\n        if (categoryNode.childCategories.isNotEmpty()) {\n            categoryNode.childCategories\n                .sortedBy { it.category.title }\n                .forEach { childCategory ->\n                    addCategoryTreeNode(node, childCategory)\n                }\n        }\n        parent.addChild(node)\n    }\n\n    private fun selectTreeNodeForCategory(parentNode: TreeNode, categoryId: String?): TreeNode? {\n        val node = parentNode.children.find { childNode ->\n            categoryId == (childNode.value as CategoryNode).category.id\n        }\n        return if (node != null) {\n            treeView.selectNode(node, true)\n            node\n        } else {\n            parentNode.children.forEach {\n                val found = selectTreeNodeForCategory(it, categoryId)\n                if (found != null) {\n                    return found\n                }\n            }\n            null\n        }\n    }\n\n    private fun onNodeSelectionChange(node: TreeNode) {\n        val wrapper = getCategoryWrapper(node)\n        if (node.isSelected) {\n            viewModel.addSelectedCategory(wrapper)\n        } else {\n            viewModel.removeSelectedCategory(wrapper)\n        }\n        updateSelectionCounter()\n    }\n\n    private fun getCategoryWrapper(childNode: TreeNode): CategoryPrefWrapper {\n        val parentCategories = findRootCategoryNodes(childNode)\n        val parentIds = parentCategories.map { (it.value as CategoryNode).category.id }\n        val category = (childNode.value as CategoryNode).category\n        return CategoryPrefWrapper(category.id, category.title, category.slug, parentIds)\n    }\n\n    override fun onDismiss(dialog: DialogInterface) {\n        super.onDismiss(dialog)\n        viewModel.storeSelectedCategories()\n        onDismissListener?.onDismiss(dialog)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        viewModel.treeViewSavedState = treeView.saveState\n    }\n\n    private fun updateSelectionCounter(parentNode: TreeNode = treeRoot) {\n        parentNode.children.forEach { child ->\n            val categoryNode = child.value as CategoryNode\n            if (categoryNode.hasChildren()) {\n                categoryNode.category.id.let { id ->\n                    val selectedChildren = viewModel.countSelectedChildrenForParent(id)\n                    val viewHolder = child.viewHolder as CategoryViewHolder\n                    viewHolder.onSelectionCounterUpdate(selectedChildren)\n                }\n                updateSelectionCounter(child)\n            }\n        }\n    }\n\n    private fun findRootCategoryNodes(childNode: TreeNode, targetLevel: Int = 1): List<TreeNode> {\n        val parentList = mutableListOf<TreeNode>()\n        var node: TreeNode = childNode\n        while (node.parent != null) {\n            val parent = node.parent\n            parentList.add(parent)\n            if (parent.level == targetLevel) {\n                break\n            }\n            node = parent\n        }\n        return parentList\n    }\n\n    private fun toggleLoadingLayout(isVisible: Boolean) {\n        binding.layoutLoading.apply {\n            root.isVisible = isVisible\n            btnRetry.isVisible = false\n            tvError.isVisible = false\n        }\n    }\n\n    private fun displayLoadingError(e: Throwable) {\n        val errorMsg = \"Error: ${e.message}\"\n        if (treeRoot.children.isEmpty()) {\n            binding.layoutLoading.apply {\n                root.isVisible = true\n                progressBar.isVisible = false\n                tvError.isVisible = true\n                tvError.text = errorMsg\n                btnRetry.isVisible = true\n            }\n        } else {\n            Snackbar.make(binding.root, \"Error: $errorMsg\", Snackbar.LENGTH_LONG).show()\n        }\n    }\n\n    private fun onMenuItemClicked(item: MenuItem): Boolean {\n        if (item.itemId == R.id.unselect_all) {\n            treeView.deselectAll()\n            viewModel.clearSelectedCategories()\n            updateSelectionCounter()\n        } else {\n            return false\n        }\n        return true\n    }\n\n    fun setOnDismissListener(listener: DialogInterface.OnDismissListener) {\n        onDismissListener = listener\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n\n    companion object {\n        private const val TAG = \"categories_dialog\"\n\n        fun showDialog(fragmentManager: FragmentManager): CategoriesDialogFragment {\n            val fragment = CategoriesDialogFragment()\n            fragment.show(fragmentManager, TAG)\n            return fragment\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/search/categories/CategoriesViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.search.categories\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.data.presentation.model.media.category.CategoryNode\nimport io.github.drumber.kitsune.data.repository.CategoryRepository\nimport io.github.drumber.kitsune.preference.CategoryPrefWrapper\nimport io.github.drumber.kitsune.data.common.Filter\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.util.logE\nimport io.github.drumber.kitsune.util.network.ResponseData\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass CategoriesViewModel(private val categoryRepository: CategoryRepository) : ViewModel() {\n\n    var treeViewSavedState: String? = null\n\n    private val _selectedCategories: MutableSet<CategoryPrefWrapper> = KitsunePref.searchCategories.toMutableSet()\n    val selectedCategories: Set<CategoryPrefWrapper>\n        get() = _selectedCategories\n\n    fun storeSelectedCategories() {\n        KitsunePref.searchCategories = selectedCategories.toList()\n    }\n\n    fun addSelectedCategory(category: CategoryPrefWrapper) {\n        _selectedCategories.add(category)\n    }\n\n    fun removeSelectedCategory(category: CategoryPrefWrapper) {\n        _selectedCategories.remove(category)\n    }\n\n    fun clearSelectedCategories() {\n        _selectedCategories.clear()\n    }\n\n    fun countSelectedChildrenForParent(parentId: String): Int {\n        return selectedCategories.filter { it.parentIds?.contains(parentId) == true }.size\n    }\n\n    private val _categoryNodes = MutableLiveData<ResponseData<List<CategoryNode>>>()\n\n    val categoryNodes: LiveData<ResponseData<List<CategoryNode>>>\n        get() = _categoryNodes\n\n    fun fetchChildCategories(parent: CategoryNode?) {\n        val parentId = parent?.category?.id ?: \"_none\"\n        val filter = Filter()\n            .filter(\"parent_id\", parentId)\n            .pageLimit(500)\n\n        viewModelScope.launch(Dispatchers.IO) {\n            try {\n                categoryRepository.getAllCategories(filter)?.let { categories ->\n                    val nodes = categories.map {\n                        CategoryNode(it)\n                    }\n\n                    if (parent == null) {\n                        _categoryNodes.postValue(ResponseData.Success(nodes))\n                    } else {\n                        parent.childCategories.addAll(0, nodes)\n                        if (_categoryNodes.value is ResponseData.Success || _categoryNodes.value?.data == null) {\n                            _categoryNodes.postValue(_categoryNodes.value)\n                        } else {\n                            val responseData = ResponseData.Success(categoryNodes.value?.data!!)\n                            _categoryNodes.postValue(responseData)\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                logE(\"Failed to fetch categories.\", e)\n                _categoryNodes.postValue(ResponseData.Error(e, categoryNodes.value?.data))\n            }\n        }\n    }\n\n    init {\n        fetchChildCategories(null)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/search/categories/CategoryViewHolder.kt",
    "content": "package io.github.drumber.kitsune.ui.search.categories\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport androidx.core.view.isVisible\nimport com.unnamed.b.atv.model.TreeNode\nimport io.github.drumber.kitsune.data.presentation.model.media.category.CategoryNode\nimport io.github.drumber.kitsune.databinding.ItemCategoryNodeBinding\n\nclass CategoryViewHolder(\n    context: Context,\n    private val callback: (CategoryNode) -> Unit\n) : TreeNode.BaseNodeViewHolder<CategoryNode>(context) {\n\n    private lateinit var binding: ItemCategoryNodeBinding\n\n    var onSelectionChangeListener: ((TreeNode) -> Unit)? = null\n\n    override fun createNodeView(node: TreeNode, value: CategoryNode): View {\n        binding = ItemCategoryNodeBinding.inflate(LayoutInflater.from(context), null, false)\n        binding.apply {\n            tvName.text = value.category.title\n            ivExpand.visibility = if(value.hasChildren()) View.VISIBLE else View.INVISIBLE\n            divider.isVisible = !node.isFirstChild\n            checkbox.isVisible = node.level > 1\n            checkbox.isChecked = node.isSelected\n\n            root.setOnClickListener {\n                if(value.hasChildren()) {\n                    ivExpand.rotation = if(node.isExpanded) 180f else 0f\n                    callback(value)\n                    tView.toggleNode(node)\n                } else {\n                    checkbox.isChecked = !checkbox.isChecked\n                }\n            }\n\n            checkbox.setOnCheckedChangeListener { button, isChecked ->\n                node.isSelected = isChecked\n                onSelectionChangeListener?.invoke(node)\n            }\n        }\n        return binding.root\n    }\n\n    fun onSelectionCounterUpdate(selectedChildren: Int) {\n        if(!this::binding.isInitialized) return\n        binding.tvCounter.apply {\n            text = if (selectedChildren < 100) {\n                selectedChildren.toString()\n            } else {\n                \"99+\"\n            }\n            isVisible = selectedChildren > 0\n        }\n    }\n\n    override fun toggle(active: Boolean) {\n        binding.ivExpand.rotation = if(active) 180f else 0f\n    }\n\n    override fun toggleSelectionMode(editModeEnabled: Boolean) {\n        binding.apply {\n            checkbox.isChecked = mNode.isSelected\n            checkbox.jumpDrawablesToCurrentState() // prevent checkbox animation\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/search/filter/FacetFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.search.filter\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Button\nimport android.widget.TextView\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.Fragment\nimport androidx.navigation.findNavController\nimport androidx.navigation.fragment.findNavController\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.algolia.instantsearch.android.filter.facet.FacetListAdapter\nimport com.algolia.instantsearch.android.list.autoScrollToStart\nimport com.algolia.instantsearch.core.connection.ConnectionHandler\nimport com.algolia.instantsearch.filter.facet.connectView\nimport com.google.android.material.divider.MaterialDividerItemDecoration\nimport com.google.android.material.navigation.NavigationBarView\nimport com.google.android.material.slider.RangeSlider\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.FragmentFilterFacetBinding\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.component.ExpandableLayout\nimport io.github.drumber.kitsune.ui.component.algolia.range.IntNumberRangeView\nimport io.github.drumber.kitsune.ui.component.algolia.range.connectView\nimport io.github.drumber.kitsune.ui.main.FragmentDecorationPreference\nimport io.github.drumber.kitsune.ui.search.SearchViewModel\nimport io.github.drumber.kitsune.ui.search.SearchViewModel.SearchClientStatus.Error\nimport io.github.drumber.kitsune.ui.search.SearchViewModel.SearchClientStatus.Initialized\nimport io.github.drumber.kitsune.ui.search.SearchViewModel.SearchClientStatus.NotAvailable\nimport io.github.drumber.kitsune.ui.search.SearchViewModel.SearchClientStatus.NotInitialized\nimport io.github.drumber.kitsune.ui.search.categories.CategoriesDialogFragment\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport org.koin.androidx.viewmodel.ext.android.activityViewModel\n\nclass FacetFragment : Fragment(R.layout.fragment_filter_facet),\n    FragmentDecorationPreference,\n    NavigationBarView.OnItemReselectedListener {\n\n    override val hasTransparentStatusBar = true\n\n    private var _binding: FragmentFilterFacetBinding? = null\n    private val binding get() = _binding!!\n\n    private val connection = ConnectionHandler()\n\n    private val viewModel: SearchViewModel by activityViewModel()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentFilterFacetBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        binding.toolbar.apply {\n            initWindowInsetsListener(false)\n            setNavigationOnClickListener { findNavController().navigateUp() }\n            setOnMenuItemClickListener { onMenuItemClicked(it) }\n        }\n\n        binding.nsvContent.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            consume = false\n        )\n\n        viewModel.filterFacets.observe(viewLifecycleOwner) { filterFacets ->\n            createFilterViews(filterFacets)\n        }\n\n        binding.layoutSearchProviderStatus.btnRetrySearchProvider.setOnClickListener {\n            viewModel.initializeSearchClient()\n        }\n\n        viewModel.searchClientStatus.observe(viewLifecycleOwner) { status ->\n            binding.apply {\n                nsvContent.isVisible = status == Initialized\n                layoutSearchProviderStatus.apply {\n                    root.isVisible = status != Initialized\n                    btnRetrySearchProvider.isVisible = status == Error || status == NotAvailable\n                    tvStatus.isVisible = btnRetrySearchProvider.isVisible\n                    progressBarSearchProvider.isVisible = status == NotInitialized\n                }\n            }\n        }\n\n        binding.toolbar.menu.findItem(R.id.menu_reset_filter).isVisible = false\n        viewModel.filtersLiveData.observe(viewLifecycleOwner) { filters ->\n            val filterCount = filters?.getFilters()?.size ?: 0\n            binding.toolbar.menu.findItem(R.id.menu_reset_filter).isVisible = filterCount > 0\n            updateCategoriesCounter()\n        }\n\n        initCategoriesCard()\n    }\n\n    private fun onMenuItemClicked(menuItem: MenuItem): Boolean {\n        return when (menuItem.itemId) {\n            R.id.menu_reset_filter -> {\n                viewModel.clearSearchFilter()\n                true\n            }\n\n            else -> false\n        }\n    }\n\n    private fun initCategoriesCard() {\n        binding.cardCategories.setOnClickListener {\n            showCategoriesDialog()\n        }\n        updateCategoriesCounter()\n    }\n\n    private fun showCategoriesDialog() {\n        parentFragmentManager.fragments.forEach { fragment ->\n            if (fragment is DialogFragment) {\n                // dismiss any open dialogs\n                fragment.dismissAllowingStateLoss()\n            }\n        }\n        val dialog = CategoriesDialogFragment.showDialog(parentFragmentManager)\n        dialog.setOnDismissListener {\n            updateCategoriesCounter()\n            viewModel.updateCategoryFilters()\n        }\n    }\n\n    private fun updateCategoriesCounter() {\n        val numCategories = KitsunePref.searchCategories.size\n        binding.tvCategoriesCounter.apply {\n            isVisible = numCategories > 0\n            text = numCategories.toString()\n        }\n    }\n\n    private fun createFilterViews(filterFacets: SearchViewModel.FilterFacets) {\n        val adapterKind = FacetListAdapter(FilterFacetListViewHolder.Factory)\n        binding.rvKind.initAdapter(adapterKind, binding.tvKind)\n        connection += filterFacets.kindConnector.connectView(\n            adapterKind,\n            filterFacets.kindPresenter\n        )\n\n        val yearView = IntNumberRangeView(binding.sliderYear)\n        binding.sliderYear.attachTextView(binding.tvYearValue) { min, max ->\n            when (max) {\n                SearchViewModel.maxYear -> \"$min - ∞\"\n                else -> \"$min - $max\"\n            }\n        }\n        connection += filterFacets.yearConnector.connectView(yearView)\n\n        val avgRatingView = IntNumberRangeView(binding.sliderAvgRating)\n        binding.sliderAvgRating.attachTextView(binding.tvAvgRatingValue) { min, max ->\n            \"$min% - $max%\"\n        }\n        connection += filterFacets.avgRatingConnector.connectView(avgRatingView)\n\n        val adapterSeason = FacetListAdapter(FilterFacetListViewHolder.Factory)\n        binding.rvSeason.initAdapter(adapterSeason, binding.tvSeason)\n        connection += filterFacets.seasonConnector.connectView(\n            adapterSeason,\n            filterFacets.seasonPresenter\n        )\n\n        val adapterSubtype = FacetListAdapter(FilterFacetListViewHolder.Factory)\n        binding.rvSubtype.initAdapter(adapterSubtype, binding.tvSubtype)\n        binding.wrapperSubtype.connectButton(binding.btnExpandSubtype)\n        connection += filterFacets.subtypeConnector.connectView(\n            adapterSubtype,\n            filterFacets.subtypePresenter\n        )\n\n        val adapterStreamers = FacetListAdapter(FilterFacetListViewHolder.Factory)\n        binding.rvStreamers.initAdapter(adapterStreamers, binding.tvStreamers)\n        binding.wrapperStreamers.connectButton(binding.btnExpandStreamers)\n        connection += filterFacets.streamersConnector.connectView(\n            adapterStreamers,\n            filterFacets.streamersPresenter\n        )\n\n        val adapterAgeRating = FacetListAdapter(FilterFacetListViewHolder.Factory)\n        binding.rvAgeRating.initAdapter(adapterAgeRating, binding.tvAgeRating)\n        connection += filterFacets.ageRatingConnector.connectView(\n            adapterAgeRating,\n            filterFacets.ageRatingPresenter\n        )\n    }\n\n    private fun RecyclerView.initAdapter(adapter: RecyclerView.Adapter<*>, label: View? = null) {\n        this.adapter = adapter\n        layoutManager = LinearLayoutManager(requireContext())\n        val divider = MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)\n        addItemDecoration(divider)\n        autoScrollToStart(adapter)\n        if (label != null) {\n            adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {\n                override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {\n                    label.isVisible = adapter.itemCount > 0\n                }\n\n                override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {\n                    label.isVisible = adapter.itemCount > 0\n                }\n            })\n        }\n    }\n\n    private fun ExpandableLayout.connectButton(button: Button) {\n        button.apply {\n            setOnClickListener { toggle() }\n            setText(if (isExpanded()) R.string.action_show_less else R.string.action_show_more)\n        }\n        expandedState.observe(viewLifecycleOwner) { expanded ->\n            button.setText(if (expanded) R.string.action_show_less else R.string.action_show_more)\n        }\n        addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->\n            button.isVisible = minHeight.toInt() <= measuredHeight\n        }\n    }\n\n    private fun RangeSlider.attachTextView(textView: TextView, getText: (min: Int, max: Int) -> String) {\n        val updateText = { slider: RangeSlider ->\n            val valueMin = slider.values[0].toInt()\n            val valueMax = slider.values[1].toInt()\n            textView.text = getText(valueMin, valueMax)\n        }\n        addOnChangeListener { slider, _, _ ->\n            updateText(slider)\n        }\n        if (values.size >= 2) {\n            updateText(this)\n        }\n    }\n\n    override fun onNavigationItemReselected(item: MenuItem) {\n        if (binding.nsvContent.canScrollVertically(-1)) {\n            binding.nsvContent.smoothScrollTo(0, 0)\n        } else {\n            findNavController().navigateUp()\n        }\n    }\n\n    override fun onDestroyView() {\n        connection.clear()\n        super.onDestroyView()\n        _binding = null\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/search/filter/FilterFacetListViewHolder.kt",
    "content": "package io.github.drumber.kitsune.ui.search.filter\n\nimport android.view.View\nimport android.view.ViewGroup\nimport com.algolia.instantsearch.android.filter.facet.FacetListViewHolder\nimport com.algolia.instantsearch.android.inflate\nimport com.algolia.search.model.search.Facet\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.ItemFacetBinding\n\nclass FilterFacetListViewHolder(view: View) : FacetListViewHolder(view) {\n\n    override fun bind(facet: Facet, selected: Boolean, onClickListener: View.OnClickListener) {\n        val binding = ItemFacetBinding.bind(view)\n        view.setOnClickListener(onClickListener)\n        binding.apply {\n            facetCount.text = facet.count.toString()\n            facetCount.visibility = View.VISIBLE\n            icon.visibility = if (selected) View.VISIBLE else View.INVISIBLE\n            facetName.text = facet.value.replaceFirstChar(Char::titlecase)\n        }\n    }\n\n    object Factory : FacetListViewHolder.Factory {\n\n        override fun createViewHolder(parent: ViewGroup): FacetListViewHolder {\n            return FilterFacetListViewHolder(parent.inflate(R.layout.item_facet))\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/settings/AppLogsFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.settings\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.content.FileProvider\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.lifecycleScope\nimport androidx.navigation.fragment.findNavController\nimport com.google.android.material.color.MaterialColors\nimport com.google.android.material.transition.MaterialSharedAxis\nimport io.github.drumber.kitsune.BuildConfig\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.FragmentAppLogsBinding\nimport io.github.drumber.kitsune.util.LogCatReader\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.viewmodel.ext.android.viewModel\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Date\n\nclass AppLogsFragment : Fragment(R.layout.fragment_app_logs) {\n\n    private var _binding: FragmentAppLogsBinding? = null\n    private val binding get() = _binding!!\n    private val viewModel: AppLogsViewModel by viewModel()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)\n        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentAppLogsBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        val colorBackground = MaterialColors.getColor(view, android.R.attr.colorBackground)\n        view.setBackgroundColor(colorBackground)\n\n        binding.toolbar.initWindowInsetsListener(consume = false)\n        binding.nestedScrollView.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            bottom = true,\n            consume = false\n        )\n\n        binding.toolbar.setNavigationOnClickListener {\n            findNavController().navigateUp()\n        }\n\n        binding.toolbar.setOnMenuItemClickListener { menuItem ->\n            if (menuItem.itemId == R.id.menu_share_app_logs) {\n                shareLogFile()\n            }\n            true\n        }\n\n        viewModel.logMessages.observe(viewLifecycleOwner) { logMessages ->\n            binding.apply {\n                progressBar.isVisible = false\n                tvNoLogs.isVisible = logMessages.isBlank()\n                tvLogMessages.isVisible = logMessages.isNotBlank()\n                tvLogMessages.text = logMessages\n\n                if (savedInstanceState == null) {\n                    nestedScrollView.post {\n                        if (!isAdded) return@post\n                        nestedScrollView.fullScroll(View.FOCUS_DOWN)\n                    }\n                }\n            }\n        }\n    }\n\n    @SuppressLint(\"SimpleDateFormat\")\n    private fun shareLogFile() {\n        val dateTime = SimpleDateFormat(\"yyy-MM-dd_HH-mm-ss\").format(Date())\n        val fileName = \"Kitsune_$dateTime.txt\"\n        val logsDir = File(requireContext().cacheDir, \"logs\")\n        val logFile = File(logsDir, fileName)\n\n        deleteAllFiles(logsDir) // delete any previously created log files\n        logFile.deleteOnExit()\n\n        lifecycleScope.launch {\n            LogCatReader.writeAppLogsToFile(logFile)\n\n            val contentUri = FileProvider.getUriForFile(\n                requireContext(),\n                \"${BuildConfig.APPLICATION_ID}.fileprovider\",\n                logFile\n            )\n            val shareIntent = Intent(Intent.ACTION_SEND).apply {\n                type = \"text/*\"\n                putExtra(Intent.EXTRA_STREAM, contentUri)\n            }\n            startActivity(\n                Intent.createChooser(\n                    shareIntent,\n                    getText(R.string.action_share_app_logs)\n                )\n            )\n        }\n    }\n\n    private fun deleteAllFiles(directory: File) {\n        directory.listFiles { file: File -> file.isFile }?.forEach { it.delete() }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/settings/AppLogsViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.settings\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.util.LogCatReader\nimport kotlinx.coroutines.launch\n\nclass AppLogsViewModel : ViewModel() {\n\n    private var _logMessages = MutableLiveData<String>()\n    val logMessages: LiveData<String>\n        get() = _logMessages\n\n    init {\n        viewModelScope.launch {\n            val logCatMessages = LogCatReader.readAppLogs()\n            _logMessages.value = logCatMessages.joinToString(\"\\n\")\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/settings/AppearanceFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.settings\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.preference.ListPreference\nimport androidx.preference.SwitchPreferenceCompat\nimport com.google.android.material.color.DynamicColors\nimport com.google.android.material.color.MaterialColors\nimport com.google.android.material.transition.MaterialSharedAxis\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.AppTheme\nimport io.github.drumber.kitsune.constants.MediaItemSize\nimport io.github.drumber.kitsune.domain.work.UpdateLibraryWidgetUseCase\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.base.BasePreferenceFragment\nimport org.koin.android.ext.android.inject\n\nclass AppearanceFragment : BasePreferenceFragment(R.string.nav_appearance) {\n\n    private val updateLibraryWidget: UpdateLibraryWidgetUseCase by inject()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)\n        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val colorBackground = MaterialColors.getColor(view, android.R.attr.colorBackground)\n        view.setBackgroundColor(colorBackground)\n    }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        preferenceManager.sharedPreferencesName = getString(R.string.preference_file_key)\n        setPreferencesFromResource(R.xml.appearance_preferences, rootKey)\n\n        //---- Dynamic Color Theme\n        findPreference<SwitchPreferenceCompat>(R.string.preference_key_dynamic_color_theme)?.apply {\n            isVisible = DynamicColors.isDynamicColorAvailable()\n            setOnPreferenceChangeListener { _, newValue ->\n                KitsunePref.useDynamicColorTheme = newValue as Boolean\n                updateLibraryWidget(context)\n                true\n            }\n        }\n\n        //---- App Theme\n        findPreference<ThemePickerPreference>(R.string.preference_key_app_theme)?.apply {\n            isEnabled = !KitsunePref.useDynamicColorTheme\n            val themeEntries = getThemePreferenceEntries()\n            setThemeEntries(themeEntries)\n            setSelectedTheme(KitsunePref.appTheme.toThemeEntry())\n            setOnPreferenceChangeListener { _, newValue ->\n                val themeIndex = themeEntries.indexOf(newValue as ThemePickerPreference.ThemeEntry)\n                KitsunePref.appTheme = AppTheme.entries[themeIndex]\n                updateLibraryWidget(context)\n                true\n            }\n        }\n\n        //---- Dark Mode\n        findPreference<ListPreference>(R.string.preference_key_dark_mode)?.apply {\n            value = KitsunePref.darkMode\n            setOnPreferenceChangeListener { _, newValue ->\n                if (KitsunePref.darkMode != newValue) {\n                    KitsunePref.darkMode = newValue as String\n                    AppCompatDelegate.setDefaultNightMode(newValue.toInt())\n                }\n                true\n            }\n        }\n\n\n        //---- OLED Black Mode\n        findPreference<SwitchPreferenceCompat>(R.string.preference_key_oled_black_mode)?.apply {\n            isEnabled = !KitsunePref.useDynamicColorTheme\n            setOnPreferenceChangeListener { _, newValue ->\n                KitsunePref.oledBlackMode = newValue as Boolean\n                true\n            }\n        }\n\n        //---- Media Item Size\n        findPreference<ListPreference>(R.string.preference_key_media_item_size)?.apply {\n            entryValues = MediaItemSize.entries.map { it.name }.toTypedArray()\n            value = KitsunePref.mediaItemSize.name\n            setOnPreferenceChangeListener { _, newValue ->\n                KitsunePref.mediaItemSize = MediaItemSize.valueOf(newValue as String)\n                true\n            }\n        }\n    }\n\n    private fun getThemePreferenceEntries(): List<ThemePickerPreference.ThemeEntry> {\n        return AppTheme.entries.map { it.toThemeEntry() }\n    }\n\n    private fun AppTheme.toThemeEntry() = when (this) {\n        AppTheme.DEFAULT -> ThemePickerPreference.ThemeEntry(\n            name = R.string.preference_app_theme_default,\n            primaryColor = R.color.md_theme_primary,\n            secondaryColor = R.color.md_theme_secondary,\n            surfaceColor = R.color.md_theme_surface\n        )\n\n        AppTheme.PURPLE -> ThemePickerPreference.ThemeEntry(\n            name = R.string.preference_app_theme_purple,\n            primaryColor = R.color.md_purple_theme_primary,\n            secondaryColor = R.color.md_purple_theme_secondary,\n            surfaceColor = R.color.md_purple_theme_surface\n        )\n        \n        AppTheme.BLUE -> ThemePickerPreference.ThemeEntry(\n            name = R.string.preference_app_theme_blue,\n            primaryColor = R.color.md_blue_theme_primary,\n            secondaryColor = R.color.md_blue_theme_secondary,\n            surfaceColor = R.color.md_blue_theme_surface\n        )\n\n        AppTheme.GREEN -> ThemePickerPreference.ThemeEntry(\n            name = R.string.preference_app_theme_green,\n            primaryColor = R.color.md_green_theme_primary,\n            secondaryColor = R.color.md_green_theme_secondary,\n            surfaceColor = R.color.md_green_theme_surface\n        )\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/settings/OSLibrariesFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.settings\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.navigation.findNavController\nimport com.google.android.material.color.MaterialColors\nimport com.google.android.material.transition.MaterialSharedAxis\nimport com.mikepenz.aboutlibraries.LibsBuilder\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.FragmentOsLibrariesBinding\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\n\nclass OSLibrariesFragment : Fragment(R.layout.fragment_os_libraries) {\n\n    private var _binding: FragmentOsLibrariesBinding? = null\n    private val binding get() = _binding!!\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)\n        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentOsLibrariesBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val colorBackground = MaterialColors.getColor(view, android.R.attr.colorBackground)\n        view.setBackgroundColor(colorBackground)\n\n        binding.collapsingToolbar.initWindowInsetsListener(consume = false)\n        binding.toolbar.apply {\n            initWindowInsetsListener(consume = false)\n            setNavigationOnClickListener { findNavController().navigateUp() }\n        }\n\n        val aboutLibrariesFragment = LibsBuilder()\n            .withLicenseShown(true)\n            .withEdgeToEdge(true)\n            .withShowLoadingProgress(true)\n            .supportFragment()\n\n        childFragmentManager.beginTransaction()\n            .replace(R.id.os_libraries_fragment_container, aboutLibrariesFragment)\n            .commit()\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/settings/SettingsFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.settings\n\nimport android.os.Bundle\nimport android.view.View\nimport android.widget.Toast\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.annotation.StringRes\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.core.os.LocaleListCompat\nimport androidx.core.view.isVisible\nimport androidx.lifecycle.lifecycleScope\nimport androidx.navigation.fragment.findNavController\nimport androidx.preference.EditTextPreference\nimport androidx.preference.ListPreference\nimport androidx.preference.ListPreference.SimpleSummaryProvider\nimport androidx.preference.Preference\nimport androidx.preference.SwitchPreferenceCompat\nimport com.google.android.material.color.MaterialColors\nimport com.google.android.material.snackbar.Snackbar\nimport com.google.android.material.transition.MaterialSharedAxis\nimport io.github.drumber.kitsune.AppLocales\nimport io.github.drumber.kitsune.BuildConfig\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.Kitsu\nimport io.github.drumber.kitsune.data.presentation.model.appupdate.UpdateCheckResult\nimport io.github.drumber.kitsune.data.repository.AppUpdateRepository\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalSfwFilterPreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalTitleLanguagePreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.databinding.FragmentPreferenceBinding\nimport io.github.drumber.kitsune.notification.Notifications\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.preference.StartPagePref\nimport io.github.drumber.kitsune.ui.base.BasePreferenceFragment\nimport io.github.drumber.kitsune.ui.permissions.isNotificationPermissionGranted\nimport io.github.drumber.kitsune.ui.permissions.requestNotificationPermission\nimport io.github.drumber.kitsune.util.extensions.navigateSafe\nimport io.github.drumber.kitsune.util.extensions.openUrl\nimport io.github.drumber.kitsune.util.ui.initMarginWindowInsetsListener\nimport kotlinx.coroutines.launch\nimport org.koin.android.ext.android.inject\nimport org.koin.androidx.viewmodel.ext.android.viewModel\nimport java.util.Locale\n\nclass SettingsFragment : BasePreferenceFragment() {\n\n    private val viewModel: SettingsViewModel by viewModel()\n\n    private val appUpdateRepository: AppUpdateRepository by inject()\n\n    // this result listener will be called on requesting notification permission after the\n    // 'check for updates on launch' permission was changed and notification permission is not granted\n    private lateinit var requestNotificationPermissionLauncher: ActivityResultLauncher<String>\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)\n        reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)\n\n        requestNotificationPermissionLauncher =\n            registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->\n                val preference =\n                    findPreference<SwitchPreferenceCompat>(R.string.preference_key_check_for_updates_on_start)\n                if (isGranted) {\n                    KitsunePref.flagUserDeniedNotificationPermission = false\n                } else {\n                    preference?.isChecked = false\n                    KitsunePref.flagUserDeniedNotificationPermission = true\n                    Toast.makeText(\n                        requireContext(),\n                        R.string.error_requires_notification_permission,\n                        Toast.LENGTH_LONG\n                    ).show()\n                }\n            }\n    }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        preferenceManager.sharedPreferencesName = getString(R.string.preference_file_key)\n        setPreferencesFromResource(R.xml.app_preferences, rootKey)\n\n        //---- Appearance\n        findPreference<Preference>(R.string.preference_key_fragment_appearance)?.setOnPreferenceClickListener {\n            val action =\n                SettingsFragmentDirections.actionSettingsFragmentToAppearanceFragment()\n            findNavController().navigate(action)\n            true\n        }\n\n        //---- App Language\n        findPreference<ListPreference>(R.string.preference_key_language)?.apply {\n            val supportedLocales = AppLocales.SUPPORTED_LOCALES\n            val selectedLocale = AppCompatDelegate.getApplicationLocales()\n                .getFirstMatch(supportedLocales)\n            val selectedLocaleValue =\n                supportedLocales.find { Locale.forLanguageTag(it).language == selectedLocale?.language }\n            val languageDisplayNames = supportedLocales.map {\n                Locale.forLanguageTag(it)\n                    .getDisplayLanguage(selectedLocale ?: Locale.getDefault())\n            }.toTypedArray()\n            entryValues = arrayOf(\"\", *supportedLocales)\n            entries = arrayOf(\n                getString(R.string.preference_language_default),\n                *languageDisplayNames\n            )\n            value = selectedLocaleValue ?: \"\"\n            setOnPreferenceChangeListener { _, newValue ->\n                val localeList = when (newValue.toString()) {\n                    \"\" -> LocaleListCompat.getEmptyLocaleList()\n                    else -> LocaleListCompat.forLanguageTags(newValue.toString())\n                }\n                AppCompatDelegate.setApplicationLocales(localeList)\n                true\n            }\n        }\n\n        //---- Start Fragment\n        findPreference<ListPreference>(R.string.preference_key_start_fragment)?.apply {\n            entryValues = StartPagePref.entries.map { it.name }.toTypedArray()\n            value = KitsunePref.startFragment.name\n            setSummaryProvider {\n                getString(R.string.preference_start_fragment_description, entry)\n            }\n            setOnPreferenceChangeListener { _, newValue ->\n                KitsunePref.startFragment = StartPagePref.valueOf(newValue as String)\n                true\n            }\n        }\n\n        //---- Force legacy image picker\n        findPreference<SwitchPreferenceCompat>(R.string.preference_key_force_legacy_image_picker)?.isVisible =\n            ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(requireContext())\n\n        //---- Check for Updates on Launch\n        findPreference<SwitchPreferenceCompat>(R.string.preference_key_check_for_updates_on_start)\n            ?.setOnPreferenceChangeListener { _, newValue ->\n                if (newValue as Boolean && !requireContext().isNotificationPermissionGranted()) {\n                    requireActivity().requestNotificationPermission(\n                        requestNotificationPermissionLauncher\n                    )\n                    return@setOnPreferenceChangeListener false\n                }\n                true\n            }\n\n        //---- App Logs\n        findPreference<Preference>(R.string.preference_key_app_logs)?.setOnPreferenceClickListener {\n            val action = SettingsFragmentDirections.actionSettingsFragmentToAppLogsFragment()\n            findNavController().navigateSafe(R.id.settingsFragment, action)\n            true\n        }\n\n        //---- App Version\n        val appVersion = \"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\"\n        findPreference<Preference>(R.string.preference_key_app_version)?.apply {\n            summary = appVersion + System.lineSeparator() +\n                    getString(R.string.preference_app_version_description)\n            setOnPreferenceClickListener {\n                checkForNewVersion()\n                true\n            }\n        }\n\n        //---- Open Source Libraries\n        findPreference<Preference>(R.string.preference_key_open_source_libraries)?.setOnPreferenceClickListener {\n            val action = SettingsFragmentDirections.actionSettingsFragmentToLibrariesFragment()\n            findNavController().navigateSafe(R.id.settingsFragment, action)\n            true\n        }\n\n        observeUserModel()\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val colorBackground = MaterialColors.getColor(view, android.R.attr.colorBackground)\n        view.setBackgroundColor(colorBackground)\n\n        val binding = FragmentPreferenceBinding.bind(view)\n\n        viewModel.errorMessageListener = {\n            Snackbar.make(view, \"Error: ${it.getMessage(requireContext())}\", Snackbar.LENGTH_LONG)\n                .setAction(R.string.action_dismiss) { /* dismiss */ }\n                .apply {\n                    this.view.initMarginWindowInsetsListener(bottom = true, consume = false)\n                }\n                .show()\n        }\n\n        viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->\n            binding.loadingOverlay.isVisible = isLoading\n        }\n    }\n\n    private fun checkForNewVersion() {\n        Toast.makeText(\n            requireContext(),\n            R.string.info_update_checking_new_version,\n            Toast.LENGTH_SHORT\n        ).show()\n\n        lifecycleScope.launch {\n            when (val result = appUpdateRepository.checkForUpdates(BuildConfig.VERSION_NAME)) {\n                is UpdateCheckResult.NewVersion -> {\n                    val release = result.release\n                    Notifications.showNewVersion(requireContext(), release)\n\n                    val message = getString(\n                        R.string.info_update_new_version_available_text,\n                        release.version\n                    )\n                    Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG)\n                        .setAction(R.string.action_view) { openUrl(release.url) }\n                        .apply {\n                            this.view.initMarginWindowInsetsListener(bottom = true, consume = false)\n                        }\n                        .show()\n                }\n\n                is UpdateCheckResult.NoNewVersion -> {\n                    Toast.makeText(\n                        requireContext(),\n                        R.string.info_update_no_new_version_available,\n                        Toast.LENGTH_SHORT\n                    ).show()\n                }\n\n                is UpdateCheckResult.Error -> {\n                    Toast.makeText(\n                        requireContext(),\n                        R.string.info_update_failed,\n                        Toast.LENGTH_SHORT\n                    ).show()\n                }\n            }\n        }\n    }\n\n    private fun observeUserModel() {\n        viewModel.userModel.observe(this) { user ->\n            //---- Title Language Preference\n            findPreference<ListPreference>(R.string.preference_key_titles)?.apply {\n                entryValues = LocalTitleLanguagePreference.entries.map { it.name }.toTypedArray()\n                setDefaultValue(KitsunePref.titles.name)\n                value = KitsunePref.titles.name\n                setOnPreferenceChangeListener { _, newValue ->\n                    val titlesPref = LocalTitleLanguagePreference.valueOf(newValue.toString())\n                    KitsunePref.titles = titlesPref\n\n                    // Title preference can be also changed without being logged in.\n                    // Do only try to update the user model if logged in.\n                    if (user != null) {\n                        updateUserIfChanged(\n                            value,\n                            newValue,\n                            LocalUser.empty(user.id).copy(\n                                titleLanguagePreference = titlesPref\n                            )\n                        )\n                    }\n                    true\n                }\n            }\n\n            //---- Country\n            findPreference<ListPreference>(R.string.preference_key_country)?.apply {\n                entryValues = Locale.getISOCountries()\n                entries =\n                    Locale.getISOCountries().map { Locale(\"\", it).displayCountry }.toTypedArray()\n                value = user?.country\n                setOnPreferenceChangeListener { _, newValue ->\n                    if (user == null) return@setOnPreferenceChangeListener false\n                    updateUserIfChanged(\n                        value,\n                        newValue,\n                        LocalUser.empty(user.id).copy(country = newValue as String)\n                    )\n                    true\n                }\n                requireUserLoggedIn(user) {\n                    if (it.value == null) {\n                        getString(R.string.preference_country_summary_non)\n                    } else {\n                        val countryName = Locale(\"\", it.value).displayName\n                        getString(R.string.preference_country_summary, countryName)\n                    }\n                }\n            }\n\n            //---- Adult Content\n            findPreference<ListPreference>(R.string.preference_key_sfw_filter)?.apply {\n                value = user?.sfwFilterPreference?.name\n                entryValues = LocalSfwFilterPreference.entries.map { it.name }.toTypedArray()\n                setOnPreferenceChangeListener { _, newValue ->\n                    if (user == null) return@setOnPreferenceChangeListener false\n                    updateUserIfChanged(\n                        value,\n                        newValue,\n                        LocalUser.empty(user.id).copy(\n                            sfwFilterPreference = LocalSfwFilterPreference.valueOf(newValue as String)\n                        )\n                    )\n                    true\n                }\n                requireUserLoggedIn(user) {\n                    val filterPreference =\n                        it.value?.let { filter -> LocalSfwFilterPreference.valueOf(filter) }\n                    getString(\n                        when (filterPreference) {\n                            LocalSfwFilterPreference.SFW -> R.string.preference_adult_content_description_sfw\n                            LocalSfwFilterPreference.NSFW_SOMETIMES -> R.string.preference_adult_content_description_sometimes\n                            LocalSfwFilterPreference.NSFW_EVERYWHERE -> R.string.preference_adult_content_description_everywhere\n                            else -> R.string.no_information\n                        }\n                    )\n                }\n            }\n\n            //---- Rating System\n            findPreference<ListPreference>(R.string.preference_key_rating_system)?.apply {\n                entryValues =\n                    LocalRatingSystemPreference.entries.reversed().map { it.name }.toTypedArray()\n                value = user?.ratingSystem?.name\n                setOnPreferenceChangeListener { _, newValue ->\n                    if (user == null) return@setOnPreferenceChangeListener false\n                    updateUserIfChanged(\n                        value,\n                        newValue,\n                        LocalUser.empty(user.id).copy(\n                            ratingSystem = LocalRatingSystemPreference.valueOf(newValue as String)\n                        )\n                    )\n                    true\n                }\n                requireUserLoggedIn(user, summaryProvider = SimpleSummaryProvider.getInstance())\n            }\n\n            //---- Display Name\n            findPreference<EditTextPreference>(R.string.preference_key_display_name)?.apply {\n                text = user?.name\n                setOnPreferenceChangeListener { _, newValue ->\n                    if (user == null) return@setOnPreferenceChangeListener false\n                    updateUserIfChanged(text, newValue, LocalUser.empty(user.id).copy(name = newValue as String))\n                    true\n                }\n                requireUserLoggedIn(user) { it.text }\n            }\n\n            //---- Profile URL\n            findPreference<EditTextPreference>(R.string.preference_key_profile_url)?.apply {\n                text = user?.slug\n                setOnPreferenceChangeListener { _, newValue ->\n                    if (user == null) return@setOnPreferenceChangeListener false\n                    updateUserIfChanged(text, newValue, LocalUser.empty(user.id).copy(slug = newValue as String))\n                    true\n                }\n                requireUserLoggedIn(user) {\n                    if (!it.text.isNullOrBlank())\n                        Kitsu.USER_URL_PREFIX + it.text\n                    else\n                        getString(R.string.preference_profile_url_not_set)\n                }\n            }\n        }\n    }\n\n    private fun updateUserIfChanged(oldValue: Any?, newValue: Any?, user: LocalUser) {\n        if (oldValue != newValue) {\n            viewModel.updateUser(user)\n        }\n    }\n\n    private inline fun <reified T : Preference> T.requireUserLoggedIn(\n        user: LocalUser?,\n        @StringRes messageRes: Int = R.string.preference_not_logged_in,\n        summaryProvider: Preference.SummaryProvider<T>? = null\n    ) {\n        isEnabled = user != null\n        if (user == null) {\n            summary = getString(messageRes)\n        }\n        this.summaryProvider = if (user != null) {\n            summaryProvider\n        } else {\n            null\n        }\n    }\n\n    override fun onDestroyView() {\n        viewModel.errorMessageListener = null\n        super.onDestroyView()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/settings/SettingsViewModel.kt",
    "content": "package io.github.drumber.kitsune.ui.settings\n\nimport android.content.Context\nimport androidx.annotation.StringRes\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.asLiveData\nimport androidx.lifecycle.map\nimport androidx.lifecycle.viewModelScope\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.exception.NoDataException\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.domain.auth.IsUserLoggedInUseCase\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass SettingsViewModel(\n    private val userRepository: UserRepository,\n    isUserLoggedIn: IsUserLoggedInUseCase\n) : ViewModel() {\n\n    val userModel = userRepository.localUser.asLiveData().map { it }\n\n    private val _isLoading = MutableLiveData(false)\n    val isLoading: LiveData<Boolean>\n        get() = _isLoading\n\n    var errorMessageListener: ((ErrorMessage) -> Unit)? = null\n\n    init {\n        if (isUserLoggedIn()) {\n            // make sure cached user data is up-to-date\n            viewModelScope.launch(Dispatchers.IO) {\n                try {\n                    userRepository.fetchAndStoreLocalUserFromNetwork()\n                } catch (e: Exception) {\n                    logE(\"Failed to update local user model from network.\", e)\n                }\n            }\n        }\n    }\n\n    fun updateUser(user: LocalUser) {\n        _isLoading.value = true\n        viewModelScope.launch(Dispatchers.IO) {\n            try {\n                userRepository.updateUser(user.id, user)\n                    ?: throw NoDataException(\"Received user data is null.\")\n                userRepository.fetchAndStoreLocalUserFromNetwork()\n            } catch (e: Exception) {\n                logE(\"Failed to update user settings.\", e)\n                errorMessageListener?.invoke(ErrorMessage(R.string.error_user_update_failed))\n                // workaround to trigger an update to reset preference values from the user model\n                (userModel as MutableLiveData).postValue(userModel.value)\n            } finally {\n                _isLoading.postValue(false)\n            }\n        }\n    }\n\n\n    data class ErrorMessage(\n        @StringRes val stringRes: Int? = null,\n        val message: String = \"\"\n    ) {\n        fun getMessage(context: Context) = if (stringRes != null) {\n            context.getString(stringRes)\n        } else {\n            message\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/settings/ThemePickerPreference.kt",
    "content": "package io.github.drumber.kitsune.ui.settings\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.annotation.ColorRes\nimport androidx.annotation.StringRes\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceViewHolder\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.databinding.ItemThemeOptionBinding\nimport io.github.drumber.kitsune.databinding.LayoutThemePickerPreferenceBinding\nimport io.github.drumber.kitsune.util.extensions.getColor\n\nclass ThemePickerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {\n\n    private lateinit var binding: LayoutThemePickerPreferenceBinding\n    private val rvAdapter = ThemeAdapter()\n    private var selectedTheme: ThemeEntry? = null\n\n    override fun onBindViewHolder(holder: PreferenceViewHolder) {\n        super.onBindViewHolder(holder)\n        binding = LayoutThemePickerPreferenceBinding.bind(holder.itemView)\n        binding.tvTitle.text = title\n        binding.rvThemes.apply {\n            layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)\n            adapter = rvAdapter\n        }\n    }\n\n    fun setThemeEntries(entries: List<ThemeEntry>) {\n        rvAdapter.apply {\n            themes.clear()\n            themes.addAll(entries)\n            notifyDataSetChanged()\n        }\n    }\n\n    fun setSelectedTheme(theme: ThemeEntry) {\n        val prevSelectedIndex = rvAdapter.themes.indexOf(selectedTheme)\n        val newSelectedIndex = rvAdapter.themes.indexOf(theme)\n        selectedTheme = theme\n        if (prevSelectedIndex != -1)\n            rvAdapter.notifyItemChanged(prevSelectedIndex)\n        if (newSelectedIndex != -1)\n            rvAdapter.notifyItemChanged(newSelectedIndex)\n    }\n\n    private fun onThemeCardClicked(theme: ThemeEntry) {\n        if (callChangeListener(theme)) {\n            setSelectedTheme(theme)\n        }\n    }\n\n    data class ThemeEntry(\n        @StringRes val name: Int,\n        @ColorRes val primaryColor: Int,\n        @ColorRes val secondaryColor: Int,\n        @ColorRes val surfaceColor: Int\n    )\n\n    private inner class ThemeAdapter(val themes: MutableList<ThemeEntry> = mutableListOf()) :\n        RecyclerView.Adapter<ThemeViewHolder>() {\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThemeViewHolder {\n            return ThemeViewHolder(\n                ItemThemeOptionBinding.inflate(\n                    LayoutInflater.from(parent.context),\n                    parent,\n                    false\n                )\n            )\n        }\n\n        override fun getItemCount(): Int = themes.size\n\n        override fun onBindViewHolder(holder: ThemeViewHolder, position: Int) {\n            holder.bind(themes[position])\n        }\n    }\n\n    private inner class ThemeViewHolder(private val binding: ItemThemeOptionBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        fun bind(theme: ThemeEntry) {\n            binding.apply {\n                cardTheme.isEnabled = isEnabled\n                cardTheme.isChecked = theme == selectedTheme\n                cardTheme.setOnClickListener { onThemeCardClicked(theme) }\n                viewPrimaryColor.setBackgroundResource(theme.primaryColor)\n                viewSecondaryColor.setBackgroundResource(theme.secondaryColor)\n                viewSurfaceColor.setBackgroundResource(theme.surfaceColor)\n                tvName.isEnabled = isEnabled\n                tvName.setText(theme.name)\n                if (isEnabled) {\n                    tvName.setTextColor(\n                        context.theme.getColor(\n                            if (theme == selectedTheme) R.attr.colorPrimary\n                            else R.attr.colorOnSurface\n                        )\n                    )\n                }\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/theme/MdcThemeAdapter.kt",
    "content": "package io.github.drumber.kitsune.ui.theme\n\nimport android.content.Context\nimport android.content.res.TypedArray\nimport androidx.annotation.StyleableRes\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.ui.graphics.Color\nimport androidx.core.content.res.getColorOrThrow\nimport androidx.core.content.res.use\nimport io.github.drumber.kitsune.R\n\nfun obtainColorScheme(\n    context: Context\n): ColorScheme {\n    return context.obtainStyledAttributes(R.styleable.MdcThemeAdapter).use { ta ->\n        val primary = ta.parseColor(R.styleable.MdcThemeAdapter_colorPrimary)\n        val onPrimary = ta.parseColor(R.styleable.MdcThemeAdapter_colorOnPrimary)\n        val primaryInverse =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorPrimaryInverse)\n        val primaryContainer =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorPrimaryContainer)\n        val onPrimaryContainer =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorOnPrimaryContainer)\n        val secondary = ta.parseColor(R.styleable.MdcThemeAdapter_colorSecondary)\n        val onSecondary = ta.parseColor(R.styleable.MdcThemeAdapter_colorOnSecondary)\n        val secondaryContainer =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorSecondaryContainer)\n        val onSecondaryContainer =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorOnSecondaryContainer)\n        val tertiary = ta.parseColor(R.styleable.MdcThemeAdapter_colorTertiary)\n        val onTertiary = ta.parseColor(R.styleable.MdcThemeAdapter_colorOnTertiary)\n        val tertiaryContainer =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorTertiaryContainer)\n        val onTertiaryContainer =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorOnTertiaryContainer)\n        val background =\n            ta.parseColor(R.styleable.MdcThemeAdapter_android_colorBackground)\n        val onBackground = ta.parseColor(R.styleable.MdcThemeAdapter_colorOnBackground)\n        val surface = ta.parseColor(R.styleable.MdcThemeAdapter_colorSurface)\n        val onSurface = ta.parseColor(R.styleable.MdcThemeAdapter_colorOnSurface)\n        val surfaceVariant =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceVariant)\n        val onSurfaceVariant =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorOnSurfaceVariant)\n        val elevationOverlay =\n            ta.parseColor(R.styleable.MdcThemeAdapter_elevationOverlayColor)\n        val surfaceInverse =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceInverse)\n        val onSurfaceInverse =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorOnSurfaceInverse)\n        val outline = ta.parseColor(R.styleable.MdcThemeAdapter_colorOutline)\n        val outlineVariant =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorOutlineVariant)\n        val error = ta.parseColor(R.styleable.MdcThemeAdapter_colorError)\n        val onError = ta.parseColor(R.styleable.MdcThemeAdapter_colorOnError)\n        val errorContainer =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorErrorContainer)\n        val onErrorContainer =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorOnErrorContainer)\n        val scrimBackground = ta.parseColor(R.styleable.MdcThemeAdapter_scrimBackground)\n        val surfaceBright = ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceBright)\n        val surfaceContainer = ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceContainer)\n        val surfaceContainerHigh =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceContainerHigh)\n        val surfaceContainerHighest =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceContainerHighest)\n        val surfaceContainerLow =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceContainerLow)\n        val surfaceContainerLowest =\n            ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceContainerLowest)\n        val surfaceDim = ta.parseColor(R.styleable.MdcThemeAdapter_colorSurfaceDim)\n\n        val isLightTheme = ta.getBoolean(R.styleable.MdcThemeAdapter_isLightTheme, true)\n\n        if (isLightTheme) {\n            lightColorScheme(\n                primary = primary,\n                onPrimary = onPrimary,\n                primaryContainer = primaryContainer,\n                onPrimaryContainer = onPrimaryContainer,\n                inversePrimary = primaryInverse,\n                secondary = secondary,\n                onSecondary = onSecondary,\n                secondaryContainer = secondaryContainer,\n                onSecondaryContainer = onSecondaryContainer,\n                tertiary = tertiary,\n                onTertiary = onTertiary,\n                tertiaryContainer = tertiaryContainer,\n                onTertiaryContainer = onTertiaryContainer,\n                background = background,\n                onBackground = onBackground,\n                surface = surface,\n                onSurface = onSurface,\n                surfaceVariant = surfaceVariant,\n                onSurfaceVariant = onSurfaceVariant,\n                surfaceTint = elevationOverlay,\n                inverseSurface = surfaceInverse,\n                inverseOnSurface = onSurfaceInverse,\n                error = error,\n                onError = onError,\n                errorContainer = errorContainer,\n                onErrorContainer = onErrorContainer,\n                outline = outline,\n                outlineVariant = outlineVariant,\n                scrim = scrimBackground,\n                surfaceBright = surfaceBright,\n                surfaceContainer = surfaceContainer,\n                surfaceContainerHigh = surfaceContainerHigh,\n                surfaceContainerHighest = surfaceContainerHighest,\n                surfaceContainerLow = surfaceContainerLow,\n                surfaceContainerLowest = surfaceContainerLowest,\n                surfaceDim = surfaceDim,\n            )\n        } else {\n            darkColorScheme(\n                primary = primary,\n                onPrimary = onPrimary,\n                primaryContainer = primaryContainer,\n                onPrimaryContainer = onPrimaryContainer,\n                inversePrimary = primaryInverse,\n                secondary = secondary,\n                onSecondary = onSecondary,\n                secondaryContainer = secondaryContainer,\n                onSecondaryContainer = onSecondaryContainer,\n                tertiary = tertiary,\n                onTertiary = onTertiary,\n                tertiaryContainer = tertiaryContainer,\n                onTertiaryContainer = onTertiaryContainer,\n                background = background,\n                onBackground = onBackground,\n                surface = surface,\n                onSurface = onSurface,\n                surfaceVariant = surfaceVariant,\n                onSurfaceVariant = onSurfaceVariant,\n                surfaceTint = elevationOverlay,\n                inverseSurface = surfaceInverse,\n                inverseOnSurface = onSurfaceInverse,\n                error = error,\n                onError = onError,\n                errorContainer = errorContainer,\n                onErrorContainer = onErrorContainer,\n                outline = outline,\n                outlineVariant = outlineVariant,\n                scrim = scrimBackground,\n                surfaceBright = surfaceBright,\n                surfaceContainer = surfaceContainer,\n                surfaceContainerHigh = surfaceContainerHigh,\n                surfaceContainerHighest = surfaceContainerHighest,\n                surfaceContainerLow = surfaceContainerLow,\n                surfaceContainerLowest = surfaceContainerLowest,\n                surfaceDim = surfaceDim,\n            )\n        }\n    }\n}\n\nprivate fun TypedArray.parseColor(@StyleableRes index: Int): Color {\n    return Color(getColorOrThrow(index))\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/theme/Theme.kt",
    "content": "package io.github.drumber.kitsune.ui.theme\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport com.google.accompanist.themeadapter.material3.createMdc3Theme\n\n@Composable\nfun KitsuneTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    dynamicColor: Boolean = true,\n    content: @Composable () -> Unit\n) {\n    val context = LocalContext.current\n    val layoutDirection = LocalLayoutDirection.current\n    val (_, typography, shapes) = createMdc3Theme(\n        context = context,\n        layoutDirection = layoutDirection,\n        readColorScheme = false\n    )\n\n    val useDynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S\n    val colorScheme = when {\n        useDynamicColor && darkTheme -> dynamicDarkColorScheme(context)\n        useDynamicColor && !darkTheme -> dynamicLightColorScheme(context)\n        else -> obtainColorScheme(context)\n    }\n\n    MaterialTheme(\n        colorScheme = colorScheme,\n        typography = typography!!,\n        shapes = shapes!!,\n        content = content\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/webview/WebViewFragment.kt",
    "content": "package io.github.drumber.kitsune.ui.webview\n\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.webkit.WebChromeClient\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.activity.addCallback\nimport androidx.core.net.toUri\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.navigation.fragment.findNavController\nimport androidx.navigation.fragment.navArgs\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.databinding.FragmentWebViewBinding\nimport io.github.drumber.kitsune.util.extensions.copyToClipboard\nimport io.github.drumber.kitsune.util.extensions.openUrl\nimport io.github.drumber.kitsune.util.ui.initPaddingWindowInsetsListener\nimport io.github.drumber.kitsune.util.ui.initWindowInsetsListener\nimport org.koin.android.ext.android.inject\n\nclass WebViewFragment : Fragment() {\n\n    private var _binding: FragmentWebViewBinding? = null\n    private val binding get() = _binding!!\n\n    private val args: WebViewFragmentArgs by navArgs()\n\n    private val accessTokenRepository: AccessTokenRepository by inject()\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        _binding = FragmentWebViewBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        binding.toolbar.apply {\n            initWindowInsetsListener(false)\n            subtitle = args.url\n            setNavigationOnClickListener {\n                findNavController().navigateUp()\n            }\n            setOnMenuItemClickListener(::onToolbarMenuItemClicked)\n        }\n\n        binding.webViewWrapper.initPaddingWindowInsetsListener(\n            left = true,\n            right = true,\n            consume = false\n        )\n\n        binding.webView.apply {\n            settings.javaScriptEnabled = true\n            settings.domStorageEnabled = true\n            webViewClient = KitsuWebViewClient(accessTokenRepository::getAccessToken)\n            webChromeClient = KitsuWebChromeClient()\n            if (savedInstanceState == null) {\n                loadUrl(args.url)\n            } else {\n                restoreState(savedInstanceState)\n            }\n        }\n\n        requireActivity().onBackPressedDispatcher.addCallback(this) {\n            if (binding.webView.canGoBack()) {\n                binding.webView.goBack()\n            } else {\n                findNavController().navigateUp()\n            }\n        }\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        binding.webView.saveState(outState)\n    }\n\n    private fun onToolbarMenuItemClicked(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.menu_open_in_browser -> {\n                binding.webView.url?.let { openUrl(it) }\n            }\n\n            R.id.menu_copy_url -> {\n                binding.webView.url?.let { copyToClipboard(\"URL\", it) }\n            }\n\n            else -> return false\n        }\n        return true\n    }\n\n    inner class KitsuWebChromeClient : WebChromeClient() {\n        override fun onReceivedTitle(view: WebView?, title: String?) {\n            super.onReceivedTitle(view, title)\n            binding.toolbar.title = title\n        }\n    }\n\n    inner class KitsuWebViewClient(\n        private val getAccessToken: () -> LocalAccessToken?\n    ) : WebViewClient() {\n\n        override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {\n            super.onPageStarted(view, url, favicon)\n\n            if (url?.toUri()?.host in validKitsuHosts) {\n                val accessToken = getAccessToken()\n                if (accessToken != null) {\n                    view?.evaluateJavascript(getAccessTokenInjectionCode(accessToken), null)\n                }\n            }\n            binding.toolbar.subtitle = url\n        }\n\n        override fun onPageFinished(view: WebView?, url: String?) {\n            super.onPageFinished(view, url)\n            binding.loadingIndicator.isVisible = false\n        }\n\n        override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {\n            super.doUpdateVisitedHistory(view, url, isReload)\n            binding.toolbar.subtitle = url\n        }\n\n        override fun shouldOverrideUrlLoading(\n            view: WebView?,\n            request: WebResourceRequest?\n        ): Boolean {\n            if (request?.url?.host in validKitsuHosts) {\n                return false\n            }\n            val browseIntent = Intent(Intent.ACTION_VIEW, request?.url)\n            startActivity(browseIntent)\n            return true\n        }\n\n        private fun getAccessTokenInjectionCode(accessToken: LocalAccessToken): String {\n            val expiresAt = (accessToken.createdAt + accessToken.expiresIn) * 1000\n            val emberSession =\n                \"{\\\"authenticated\\\":{\\\"authenticator\\\":\\\"authenticator:oauth2\\\",\\\"access_token\\\":\\\"${accessToken.accessToken}\\\",\\\"token_type\\\":\\\"Bearer\\\",\\\"expires_in\\\":${accessToken.expiresIn},\\\"refresh_token\\\":\\\"${accessToken.refreshToken}\\\",\\\"scope\\\":\\\"public\\\",\\\"created_at\\\":${accessToken.createdAt},\\\"expires_at\\\":${expiresAt}}}\"\n            return \"window.localStorage.setItem(\\\"ember_simple_auth:session\\\", '$emberSession');\"\n        }\n    }\n\n    companion object {\n        private val validKitsuHosts = listOf(\"kitsu.app\", \"kitsu.io\")\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/widget/KitsuneWidgetReceiver.kt",
    "content": "package io.github.drumber.kitsune.ui.widget\n\nimport android.appwidget.AppWidgetManager\nimport android.content.Context\nimport androidx.glance.appwidget.GlanceAppWidget\nimport androidx.glance.appwidget.GlanceAppWidgetReceiver\nimport androidx.work.Constraints\nimport androidx.work.ExistingWorkPolicy\nimport androidx.work.NetworkType\nimport androidx.work.OneTimeWorkRequestBuilder\nimport androidx.work.WorkManager\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.work.SyncLibraryEntriesForWidgetWorker\nimport kotlin.time.Duration.Companion.minutes\n\nclass KitsuneWidgetReceiver : GlanceAppWidgetReceiver() {\n    override val glanceAppWidget: GlanceAppWidget = LibraryAppWidget()\n\n    override fun onUpdate(\n        context: Context,\n        appWidgetManager: AppWidgetManager,\n        appWidgetIds: IntArray\n    ) {\n        if (shouldSyncLibrary()) {\n            enqueueSyncLibraryWork(context)\n        }\n        super.onUpdate(context, appWidgetManager, appWidgetIds)\n    }\n\n    private fun shouldSyncLibrary(): Boolean {\n        val lastSyncMillis = KitsunePref.lastLibraryFetchForWidget\n        return lastSyncMillis == -1L || System.currentTimeMillis() >= (lastSyncMillis + 5.minutes.inWholeMilliseconds)\n    }\n\n    private fun enqueueSyncLibraryWork(context: Context) {\n        val workConstraints = Constraints.Builder()\n            .setRequiredNetworkType(NetworkType.CONNECTED)\n            .build()\n        val syncWork = OneTimeWorkRequestBuilder<SyncLibraryEntriesForWidgetWorker>()\n            .setConstraints(workConstraints)\n            .build()\n        WorkManager.getInstance(context).enqueueUniqueWork(\n            SyncLibraryEntriesForWidgetWorker.TAG,\n            ExistingWorkPolicy.KEEP,\n            syncWork\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/widget/KitsuneWidgetTheme.kt",
    "content": "package io.github.drumber.kitsune.ui.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport androidx.compose.runtime.Composable\nimport androidx.glance.GlanceTheme\nimport androidx.glance.LocalContext\nimport androidx.glance.color.ColorProviders\nimport androidx.glance.color.colorProviders\nimport androidx.glance.unit.ColorProvider\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.constants.AppTheme\nimport io.github.drumber.kitsune.util.extensions.getResourceId\n\nobject KitsuneWidgetTheme {\n\n    fun Context.applyTheme(appTheme: AppTheme) {\n        setTheme(appTheme.themeRes)\n    }\n\n    @Composable\n    fun getColors(useDynamicColorTheme: Boolean): ColorProviders {\n        if (useDynamicColorTheme) {\n            return GlanceTheme.colors\n        }\n        return getColorSchemeFromAppTheme(LocalContext.current)\n    }\n\n    @SuppressLint(\"RestrictedApi\")\n    private fun getColorSchemeFromAppTheme(context: Context): ColorProviders {\n        return colorProviders(\n            primary = ColorProvider(context.theme.getResourceId(R.attr.colorPrimary)),\n            onPrimary = ColorProvider(context.theme.getResourceId(R.attr.colorOnPrimary)),\n            primaryContainer = ColorProvider(context.theme.getResourceId(R.attr.colorPrimaryContainer)),\n            onPrimaryContainer = ColorProvider(context.theme.getResourceId(R.attr.colorOnPrimaryContainer)),\n            secondary = ColorProvider(context.theme.getResourceId(R.attr.colorSecondary)),\n            onSecondary = ColorProvider(context.theme.getResourceId(R.attr.colorOnSecondary)),\n            secondaryContainer = ColorProvider(context.theme.getResourceId(R.attr.colorSecondaryContainer)),\n            onSecondaryContainer = ColorProvider(context.theme.getResourceId(R.attr.colorOnSecondaryContainer)),\n            tertiary = ColorProvider(context.theme.getResourceId(R.attr.colorTertiary)),\n            onTertiary = ColorProvider(context.theme.getResourceId(R.attr.colorOnTertiary)),\n            tertiaryContainer = ColorProvider(context.theme.getResourceId(R.attr.colorTertiaryContainer)),\n            onTertiaryContainer = ColorProvider(context.theme.getResourceId(R.attr.colorOnTertiaryContainer)),\n            error = ColorProvider(context.theme.getResourceId(R.attr.colorError)),\n            errorContainer = ColorProvider(context.theme.getResourceId(R.attr.colorErrorContainer)),\n            onError = ColorProvider(context.theme.getResourceId(R.attr.colorOnError)),\n            onErrorContainer = ColorProvider(context.theme.getResourceId(R.attr.colorOnErrorContainer)),\n            background = ColorProvider(context.theme.getResourceId(android.R.attr.colorBackground)),\n            onBackground = ColorProvider(context.theme.getResourceId(R.attr.colorOnBackground)),\n            surface = ColorProvider(context.theme.getResourceId(R.attr.colorSurface)),\n            onSurface = ColorProvider(context.theme.getResourceId(R.attr.colorOnSurface)),\n            surfaceVariant = ColorProvider(context.theme.getResourceId(R.attr.colorSurfaceVariant)),\n            onSurfaceVariant = ColorProvider(context.theme.getResourceId(R.attr.colorOnSurfaceVariant)),\n            outline = ColorProvider(context.theme.getResourceId(R.attr.colorOutline)),\n            inverseOnSurface = ColorProvider(context.theme.getResourceId(R.attr.colorOnSurfaceInverse)),\n            inverseSurface = ColorProvider(context.theme.getResourceId(R.attr.colorSurfaceInverse)),\n            inversePrimary = ColorProvider(context.theme.getResourceId(R.attr.colorPrimaryInverse)),\n            widgetBackground = ColorProvider(context.theme.getResourceId(R.attr.colorSurface)),\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/widget/LibraryAppWidget.kt",
    "content": "package io.github.drumber.kitsune.ui.widget\n\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.net.Uri\nimport android.os.Build\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.glance.Button\nimport androidx.glance.GlanceId\nimport androidx.glance.GlanceModifier\nimport androidx.glance.GlanceTheme\nimport androidx.glance.Image\nimport androidx.glance.ImageProvider\nimport androidx.glance.LocalContext\nimport androidx.glance.action.Action\nimport androidx.glance.action.clickable\nimport androidx.glance.appwidget.GlanceAppWidget\nimport androidx.glance.appwidget.LinearProgressIndicator\nimport androidx.glance.appwidget.SizeMode\nimport androidx.glance.appwidget.action.actionStartActivity\nimport androidx.glance.appwidget.components.Scaffold\nimport androidx.glance.appwidget.components.SquareIconButton\nimport androidx.glance.appwidget.cornerRadius\nimport androidx.glance.appwidget.lazy.LazyColumn\nimport androidx.glance.appwidget.lazy.items\nimport androidx.glance.appwidget.provideContent\nimport androidx.glance.appwidget.updateAll\nimport androidx.glance.background\nimport androidx.glance.layout.Alignment\nimport androidx.glance.layout.Box\nimport androidx.glance.layout.Column\nimport androidx.glance.layout.Row\nimport androidx.glance.layout.Spacer\nimport androidx.glance.layout.fillMaxSize\nimport androidx.glance.layout.fillMaxWidth\nimport androidx.glance.layout.height\nimport androidx.glance.layout.padding\nimport androidx.glance.layout.size\nimport androidx.glance.layout.width\nimport androidx.glance.layout.wrapContentHeight\nimport androidx.glance.text.FontWeight\nimport androidx.glance.text.Text\nimport androidx.glance.text.TextAlign\nimport androidx.glance.text.TextStyle\nimport androidx.lifecycle.asFlow\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.load.DataSource\nimport com.bumptech.glide.load.engine.GlideException\nimport com.bumptech.glide.load.resource.bitmap.RoundedCorners\nimport com.bumptech.glide.request.RequestListener\nimport com.bumptech.glide.request.target.Target\nimport com.chibatching.kotpref.livedata.asLiveData\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.addTransform\nimport io.github.drumber.kitsune.constants.IntentAction.OPEN_LIBRARY\nimport io.github.drumber.kitsune.constants.IntentAction.OPEN_MEDIA\nimport io.github.drumber.kitsune.constants.LibraryWidget\nimport io.github.drumber.kitsune.data.presentation.dto.toMediaDto\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.media.identifier\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository.AccessTokenState\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.domain.auth.IsUserLoggedInUseCase\nimport io.github.drumber.kitsune.domain.library.UpdateLibraryEntryProgressUseCase\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport io.github.drumber.kitsune.ui.details.DetailsFragmentArgs\nimport io.github.drumber.kitsune.ui.main.MainActivity\nimport io.github.drumber.kitsune.ui.widget.KitsuneWidgetTheme.applyTheme\nimport io.github.drumber.kitsune.util.logE\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.cancelFutureOnCancellation\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.emptyFlow\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.coroutines.withContext\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\nclass LibraryAppWidget : GlanceAppWidget(), KoinComponent {\n\n    companion object {\n        private const val POSTER_IMG_WIDTH = 60\n        private const val POSTER_IMG_HEIGHT = 85\n    }\n\n    private val isLoggedIn: IsUserLoggedInUseCase by inject()\n    private val accessTokenRepository: AccessTokenRepository by inject()\n    private val libraryRepository: LibraryRepository by inject()\n    private val updateLibraryEntryProgress: UpdateLibraryEntryProgressUseCase by inject()\n\n    override val sizeMode: SizeMode = SizeMode.Single\n\n    override suspend fun provideGlance(context: Context, id: GlanceId) {\n        val initialEntries = loadData()\n        provideContent {\n            val appTheme by KitsunePref.asLiveData(KitsunePref::appTheme)\n                .asFlow()\n                .collectAsState(initial = KitsunePref.appTheme)\n            val useDynamicColorTheme by KitsunePref.asLiveData(KitsunePref::useDynamicColorTheme)\n                .asFlow()\n                .collectAsState(initial = KitsunePref.useDynamicColorTheme)\n            LocalContext.current.applyTheme(appTheme)\n\n            val scope = rememberCoroutineScope()\n            val entries by getDataFlow().collectAsState(initial = initialEntries)\n\n            GlanceTheme(colors = KitsuneWidgetTheme.getColors(useDynamicColorTheme)) {\n                Scaffold(horizontalPadding = 0.dp) {\n                    WidgetContent(\n                        entries = entries,\n                        clickItemAction = { libraryEntry ->\n                            val intent = getMainActivityIntent(context).apply {\n                                action = OPEN_MEDIA\n                                val args = DetailsFragmentArgs(\n                                    media = libraryEntry.media?.toMediaDto(),\n                                    type = libraryEntry.media?.mediaType?.identifier,\n                                    slug = libraryEntry.media?.id\n                                )\n                                putExtras(args.toBundle())\n                            }\n                            actionStartActivity(intent)\n                        },\n                        progressAction = { libraryEntry, progress ->\n                            scope.launch {\n                                withContext(Dispatchers.IO) {\n                                    updateLibraryEntryProgress(libraryEntry, progress)\n                                }\n                                updateAll(context)\n                            }\n                        },\n                        emptyListAction = {\n                            val intent = getMainActivityIntent(context).apply {\n                                action = OPEN_LIBRARY\n                            }\n                            actionStartActivity(intent)\n                        }\n                    )\n                }\n            }\n        }\n    }\n\n    @Composable\n    private fun WidgetContent(\n        entries: List<LibraryEntryWithModification>,\n        clickItemAction: (LibraryEntry) -> Action,\n        progressAction: (LibraryEntry, Int) -> Unit,\n        emptyListAction: () -> Action\n    ) {\n        if (entries.isNotEmpty()) {\n            val padding = 8.dp\n            val itemBackgroundColor = GlanceTheme.colors.surfaceVariant\n\n            LazyColumn(\n                modifier = GlanceModifier\n                    .fillMaxSize()\n            ) {\n                items(items = entries, itemId = { item -> item.id.toLong() }) { item ->\n                    val isLastItem = entries.lastOrNull()?.id == item.id\n                    val itemCardModifier = GlanceModifier\n                        .wrapContentHeight()\n                        .fillMaxWidth()\n                        .background(itemBackgroundColor)\n                        .cornerRadiusCompat { itemBackgroundColor }\n\n                    Box(\n                        modifier = GlanceModifier.padding(\n                            start = padding,\n                            top = padding,\n                            end = padding,\n                            bottom = if (isLastItem) padding else 0.dp\n                        )\n                    ) {\n                        LibraryItem(\n                            item = item,\n                            modifier = itemCardModifier,\n                            clickItemAction = clickItemAction,\n                            progressAction = progressAction\n                        )\n                    }\n                }\n            }\n        } else {\n            EmptyView(emptyListAction)\n        }\n    }\n\n    @Composable\n    private fun LibraryItem(\n        item: LibraryEntryWithModification,\n        modifier: GlanceModifier,\n        clickItemAction: (LibraryEntry) -> Action,\n        progressAction: (LibraryEntry, Int) -> Unit\n    ) {\n        val context = LocalContext.current\n\n        val posterCornerRadius = innerCornerRadius(context)\n\n        val posterUrl = item.media?.posterImageUrl\n        var posterImage by remember(posterUrl) { mutableStateOf<Bitmap?>(null) }\n        LaunchedEffect(posterUrl) {\n            try {\n                posterImage = loadBitmap(context, posterUrl, posterCornerRadius)\n            } catch (e: Exception) {\n                logE(\"Failed to load poster image for URL $posterUrl\", e)\n            }\n        }\n\n        val imageProvider = posterImage?.let { ImageProvider(it) }\n            ?: ImageProvider(R.drawable.ic_insert_photo_48)\n\n        Row(\n            modifier = modifier.clickable(clickItemAction(item.libraryEntry))\n        ) {\n            Image(\n                provider = imageProvider,\n                contentDescription = null,\n                modifier = GlanceModifier\n                    .size(width = POSTER_IMG_WIDTH.dp, height = POSTER_IMG_HEIGHT.dp)\n                    .applyIf(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n                        cornerRadius(android.R.dimen.system_app_widget_inner_radius)\n                    }\n            )\n            Box(contentAlignment = Alignment.BottomEnd) {\n                Column(\n                    modifier = GlanceModifier\n                        .fillMaxSize()\n                        .padding(start = 12.dp, top = 12.dp, end = 12.dp, bottom = 0.dp)\n                ) {\n                    Text(\n                        text = item.media?.title ?: \"\",\n                        style = TextStyle(\n                            color = GlanceTheme.colors.onSurface,\n                            fontSize = 16.sp,\n                            fontWeight = FontWeight.Medium\n                        ),\n                        maxLines = 1\n                    )\n                    Spacer(GlanceModifier.height(4.dp))\n                    Text(\n                        text = item.media?.subtypeFormatted ?: \"\",\n                        style = TextStyle(\n                            color = GlanceTheme.colors.onSurfaceVariant,\n                            fontSize = 12.sp,\n                            fontWeight = FontWeight.Normal\n                        )\n                    )\n                }\n\n                if (item.hasEpisodesCount && item.hasStartedWatching) {\n                    val episodeCount = item.episodeCount?.coerceAtLeast(1) ?: 1\n                    val progress = item.progress ?: 0\n                    LinearProgressIndicator(\n                        progress = progress.toFloat() / episodeCount,\n                        color = GlanceTheme.colors.primary,\n                        backgroundColor = GlanceTheme.colors.secondaryContainer,\n                        modifier = GlanceModifier\n                            .fillMaxWidth()\n                            .height(4.dp)\n                            .applyIf(Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {\n                                // corner clipping is not supported on android < S\n                                // to avoid overflow of the progress bar, we add padding to the end\n                                padding(end = 12.dp)\n                            }\n                    )\n                }\n\n                Row(\n                    verticalAlignment = Alignment.Bottom,\n                    modifier = GlanceModifier.padding(bottom = 12.dp, end = 12.dp)\n                ) {\n                    val progressText = when (item.progress) {\n                        null, 0 -> context.getString(R.string.library_not_started)\n                        else -> \"${item.progress ?: 0}/${item.episodeCountFormatted}\"\n                    }\n                    Text(\n                        text = progressText,\n                        style = TextStyle(\n                            color = GlanceTheme.colors.onSurfaceVariant,\n                            fontSize = 12.sp,\n                            fontWeight = FontWeight.Normal\n                        )\n                    )\n                    Spacer(GlanceModifier.width(8.dp))\n                    SquareIconButton(\n                        imageProvider = ImageProvider(R.drawable.ic_add_24),\n                        contentDescription = null,\n                        modifier = GlanceModifier.size(48.dp),\n                        onClick = { progressAction(item.libraryEntry, item.progress?.plus(1) ?: 1) }\n                    )\n                }\n            }\n        }\n    }\n\n    @Composable\n    fun EmptyView(action: () -> Action) {\n        val context = LocalContext.current\n        Column(\n            verticalAlignment = Alignment.Vertical.CenterVertically,\n            horizontalAlignment = Alignment.Horizontal.CenterHorizontally,\n            modifier = GlanceModifier.fillMaxSize().padding(4.dp)\n        ) {\n            Text(\n                text = context.getString(R.string.widget_empty_text),\n                style = TextStyle(\n                    color = GlanceTheme.colors.onSurfaceVariant,\n                    fontSize = 16.sp,\n                    textAlign = TextAlign.Center\n                )\n            )\n            Spacer(GlanceModifier.height(10.dp))\n            Button(\n                text = context.getString(R.string.widget_empty_action),\n                onClick = action()\n            )\n        }\n    }\n\n    private suspend fun loadData(): List<LibraryEntryWithModification> {\n        if (!isLoggedIn()) return emptyList()\n        return try {\n            libraryRepository.getLibraryEntriesWithModificationsByStatus(\n                listOf(LibraryStatus.Current)\n            ).take(LibraryWidget.MAX_ITEM_COUNT)\n        } catch (e: Exception) {\n            logE(\"Failed to get library entries.\", e)\n            emptyList()\n        }\n    }\n\n    @OptIn(ExperimentalCoroutinesApi::class)\n    private fun getDataFlow(): Flow<List<LibraryEntryWithModification>> {\n        return try {\n            accessTokenRepository.accessTokenState.flatMapLatest { state ->\n                if (state == AccessTokenState.PRESENT) {\n                    libraryRepository.getLibraryEntriesWithModificationsByStatusAsFlow(\n                        listOf(LibraryStatus.Current)\n                    ).map { it.take(LibraryWidget.MAX_ITEM_COUNT) }\n                } else {\n                    emptyFlow()\n                }\n            }\n        } catch (e: Exception) {\n            logE(\"Failed to get library entries flow.\", e)\n            emptyFlow()\n        }\n    }\n\n    private suspend fun loadBitmap(\n        context: Context,\n        url: String?,\n        cornerRadius: Int\n    ) = suspendCancellableCoroutine { cont ->\n        val request = Glide.with(context)\n            .asBitmap()\n            .load(url)\n            .addTransform(RoundedCorners(cornerRadius))\n            .listener(object : RequestListener<Bitmap> {\n                override fun onLoadFailed(\n                    e: GlideException?,\n                    model: Any?,\n                    target: Target<Bitmap>,\n                    isFirstResource: Boolean\n                ): Boolean {\n                    cont.resumeWithException(e ?: Exception(\"Image failed to load.\"))\n                    return false\n                }\n\n                override fun onResourceReady(\n                    resource: Bitmap,\n                    model: Any,\n                    target: Target<Bitmap>?,\n                    dataSource: DataSource,\n                    isFirstResource: Boolean\n                ): Boolean {\n                    cont.resume(resource)\n                    return false\n                }\n            })\n            .submit()\n        cont.cancelFutureOnCancellation(request)\n    }\n\n    private fun getMainActivityIntent(context: Context): Intent {\n        return Intent(context, MainActivity::class.java).apply {\n            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK\n            data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/ui/widget/WidgetUtils.kt",
    "content": "package io.github.drumber.kitsune.ui.widget\n\nimport android.content.Context\nimport android.os.Build\nimport androidx.glance.ColorFilter\nimport androidx.glance.GlanceModifier\nimport androidx.glance.ImageProvider\nimport androidx.glance.appwidget.cornerRadius\nimport androidx.glance.background\nimport androidx.glance.unit.ColorProvider\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.util.extensions.toPx\n\nfun GlanceModifier.applyIf(\n    condition: Boolean,\n    block: GlanceModifier.() -> GlanceModifier\n): GlanceModifier {\n    return if (condition) {\n        block()\n    } else {\n        this\n    }\n}\n\nfun innerCornerRadius(context: Context, fallbackRadius: Int = 20): Int {\n    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n        context.resources.getDimensionPixelSize(android.R.dimen.system_app_widget_inner_radius)\n    } else {\n        fallbackRadius.toPx()\n    }\n}\n\nfun GlanceModifier.cornerRadiusCompat(\n    backgroundColorProvider: () -> ColorProvider\n): GlanceModifier {\n    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n        cornerRadius(android.R.dimen.system_app_widget_inner_radius)\n    } else {\n        background(\n            ImageProvider(R.drawable.widget_rounded_rect),\n            colorFilter = ColorFilter.tint(backgroundColorProvider())\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/DataUtil.kt",
    "content": "package io.github.drumber.kitsune.util\n\nimport android.content.Context\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.en\nimport io.github.drumber.kitsune.data.common.enJp\nimport io.github.drumber.kitsune.data.common.enUs\nimport io.github.drumber.kitsune.data.common.jaJp\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalTitleLanguagePreference\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport java.text.SimpleDateFormat\nimport java.util.Calendar\nimport java.util.Locale\n\nobject DataUtil {\n\n    @JvmStatic\n    fun formatDate(dateString: String?) = dateString?.parseDate()?.formatDate(SimpleDateFormat.LONG)\n\n    @JvmStatic\n    fun getGenderString(gender: String?, context: Context): String {\n        return when (gender) {\n            \"male\" -> context.getString(R.string.profile_gender_male)\n            \"female\" -> context.getString(R.string.profile_gender_female)\n            \"secret\", null -> context.getString(R.string.profile_data_private)\n            else -> gender\n        }\n    }\n\n    @JvmStatic\n    fun formatUserJoinDate(joinDate: String?, context: Context): String? {\n        return joinDate?.parseDate()?.let { dateJoined ->\n            val diffMillis = Calendar.getInstance().timeInMillis - dateJoined.time\n            val differenceString = TimeUtil.roundTime(diffMillis / 1000, context)\n            \"${dateJoined.formatDate(SimpleDateFormat.LONG)} \" +\n                    \"(${context.getString(R.string.profile_data_join_date_ago, differenceString)})\"\n        }\n    }\n\n    @JvmStatic\n    fun getTitle(title: Titles?, canonical: String?): String? {\n        return when (KitsunePref.titles) {\n            LocalTitleLanguagePreference.Canonical -> canonical.nb()\n                ?: title?.enJp.nb() ?: title?.en.nb() ?: title?.enUs.nb() ?: title?.jaJp\n            LocalTitleLanguagePreference.Romanized -> title?.enJp.nb()\n                ?: canonical.nb() ?: title?.en.nb() ?: title?.enUs.nb() ?: title?.jaJp\n            LocalTitleLanguagePreference.English -> title?.en.nb()\n                ?: title?.enUs.nb() ?: canonical.nb() ?: title?.enJp.nb() ?: title?.jaJp\n        }\n    }\n\n    @JvmStatic\n    fun <T> Map<String, T>.mapLanguageCodesToDisplayName(includeCountry: Boolean = true): Map<String, T> {\n        return mapKeys {\n            val locale = Locale.forLanguageTag(it.key.replace('_', '-'))\n            if (includeCountry && it.key.lowercase().split('_').toSet().size > 1)\n                locale.displayName\n            else\n                locale.displayLanguage\n        }\n    }\n\n    /**\n     * Maps blank strings to null.\n     */\n    private fun String?.nb() = if (this.isNullOrBlank()) null else this\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/DateUtil.kt",
    "content": "package io.github.drumber.kitsune.util\n\nimport java.text.ParseException\nimport java.text.SimpleDateFormat\nimport java.util.Calendar\nimport java.util.Date\nimport java.util.Locale\nimport java.util.TimeZone\n\nconst val DATE_FORMAT_ISO = \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"\n\nfun String.parseDate(\n    format: String = \"yyyy-MM-dd\",\n    timeZoneOfDateString: TimeZone = TimeZone.getDefault()\n): Date? {\n    if (this.isBlank()) {\n        return null\n    }\n    val dateFormat = SimpleDateFormat(format, Locale.getDefault())\n    dateFormat.timeZone = timeZoneOfDateString\n    return try {\n        dateFormat.parse(this)\n    } catch (_: ParseException) {\n        null\n    }\n}\n\nfun String.parseUtcDate(format: String = DATE_FORMAT_ISO) =\n    parseDate(format, TimeZone.getTimeZone(\"UTC\"))\n\nfun Date.formatDate(dateFormat: Int = SimpleDateFormat.DEFAULT): String {\n    val format = SimpleDateFormat.getDateInstance(dateFormat)\n    return format.format(this)\n}\n\nfun Date.formatDate(pattern: String, timeZone: TimeZone = TimeZone.getDefault()): String {\n    val format = SimpleDateFormat(pattern, Locale.getDefault())\n    format.timeZone = timeZone\n    return format.format(this)\n}\n\nfun Date.formatUtcDate(pattern: String = DATE_FORMAT_ISO) =\n    formatDate(pattern, TimeZone.getTimeZone(\"UTC\"))\n\nfun Calendar.formatDate(dateFormat: Int = SimpleDateFormat.DEFAULT) = time.formatDate(dateFormat)\n\nfun Calendar.formatDate(pattern: String, timeZone: TimeZone = TimeZone.getDefault()) =\n    time.formatDate(pattern, timeZone)\n\nfun Calendar.formatUtcDate(pattern: String = DATE_FORMAT_ISO) = time.formatUtcDate(pattern)\n\nfun getLocalCalendar(): Calendar = Calendar.getInstance()\n\nfun getUtcCalendar(rawCalendar: Calendar? = null): Calendar {\n    val calendar = Calendar.getInstance(TimeZone.getTimeZone(\"UTC\"))\n    if (rawCalendar == null) {\n        calendar.clear()\n    } else {\n        calendar.timeInMillis = rawCalendar.timeInMillis\n    }\n    return calendar\n}\n\nfun getDayCopyInUtc(rawCalendar: Calendar): Calendar {\n    val rawCalendarInUtc = getUtcCalendar(rawCalendar)\n    val utcCalendar = getUtcCalendar()\n    utcCalendar.set(\n        rawCalendarInUtc.get(Calendar.YEAR),\n        rawCalendarInUtc.get(Calendar.MONTH),\n        rawCalendarInUtc.get(Calendar.DAY_OF_MONTH)\n    )\n    return utcCalendar\n}\n\n/**\n * Keeps only year, month and day information of the specified time in milliseconds.\n */\nfun stripTimeOfUtcMillis(rawDate: Long): Long {\n    val rawCalendar = getUtcCalendar()\n    rawCalendar.timeInMillis = rawDate\n    val sanitizedStartItem = getDayCopyInUtc(rawCalendar)\n    return sanitizedStartItem.timeInMillis\n}\n\n/**\n * Keeps only year, month and day information of the specified time in milliseconds.\n */\nfun Long.stripTimeUtcMillis() = stripTimeOfUtcMillis(this)\n\nfun Long.toDate(): Date {\n    val calendar = Calendar.getInstance()\n    calendar.timeInMillis = this\n    return calendar.time\n}\n\nfun Date.toCalendar(): Calendar {\n    val calendar = Calendar.getInstance()\n    calendar.time = this\n    return calendar\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/ItemClickListener.kt",
    "content": "package io.github.drumber.kitsune.util\n\nfun interface ItemClickListener {\n    fun onItemClicked()\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/KitsuUrlReplacer.kt",
    "content": "package io.github.drumber.kitsune.util\n\n/**\n * Replaces the media URL from the old kitsu.io domain to the new kitsu.app domain.\n *\n * Added on 2024-08-11 due to sudden domain change. Algolia search results are still using the old media domain.\n * Related PR: https://github.com/Drumber/Kitsune/pull/57\n *\n * TODO: Can be removed once Kitsu has fully migrated to the new domain.\n */\nfun String.fixImageUrl() = replaceFirst(\"media.kitsu.io\", \"media.kitsu.app\")"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/LogCatReader.kt",
    "content": "package io.github.drumber.kitsune.util\n\nimport android.os.Build\nimport io.github.drumber.kitsune.BuildConfig\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.util.Date\n\nobject LogCatReader {\n\n    suspend fun readAppLogs(maxLines: Int = 5000) = withContext(Dispatchers.IO) {\n        val logLevelFilter = when (BuildConfig.DEBUG) {\n            true -> \"*:D\"\n            false -> \"*:I\"\n        }\n        val process = Runtime.getRuntime().exec(\"logcat -d -t $maxLines $logLevelFilter\")\n        process.inputStream.bufferedReader().use {\n            return@withContext it.readLines()\n        }\n    }\n\n    suspend fun writeAppLogsToFile(\n        file: File,\n        writeHeader: Boolean = true\n    ) = withContext(Dispatchers.IO) {\n        val logs = readAppLogs()\n        file.parentFile?.mkdirs()\n        file.bufferedWriter().use { writer ->\n            if (writeHeader)\n                writer.appendLine(generateLogFileHeader())\n            logs.forEach { line ->\n                writer.appendLine(line)\n            }\n        }\n    }\n\n    private fun generateLogFileHeader(): String {\n        return StringBuilder()\n            .appendLine(\"###########################################################\")\n            .appendLine(\"Log file generated on ${Date()}\")\n            .appendLine(\"Kitsune version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\")\n            .appendLine(\"Application ID: ${BuildConfig.APPLICATION_ID}\")\n            .appendLine(\"Build type: ${BuildConfig.BUILD_TYPE}\")\n            .appendLine(\"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})\")\n            .appendLine(\"Device: ${Build.MANUFACTURER} ${Build.MODEL}\")\n            .appendLine(\"###########################################################\")\n            .toString()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/LogUtil.kt",
    "content": "package io.github.drumber.kitsune.util\n\nimport android.util.Log\n\ninline fun Any.logV(msg: String, t: Throwable? = null) {\n    if (t == null)\n        Log.v(this::class.java.simpleName, msg)\n    else\n        Log.v(this::class.java.simpleName, msg, t)\n}\n\ninline fun Any.logD(msg: String, t: Throwable? = null) {\n    if (t == null)\n        Log.d(this::class.java.simpleName, msg)\n    else\n        Log.d(this::class.java.simpleName, msg, t)\n}\n\ninline fun Any.logI(msg: String, t: Throwable? = null) {\n    if (t == null)\n        Log.i(this::class.java.simpleName, msg)\n    else\n        Log.i(this::class.java.simpleName, msg, t)\n}\n\ninline fun Any.logW(msg: String, t: Throwable? = null) {\n    if (t == null)\n        Log.w(this::class.java.simpleName, msg)\n    else\n        Log.w(this::class.java.simpleName, msg, t)\n}\n\ninline fun Any.logE(msg: String, t: Throwable? = null) {\n    if (t == null)\n        Log.e(this::class.java.simpleName, msg)\n    else\n        Log.e(this::class.java.simpleName, msg, t)\n}\n\ninline fun Any.logWTF(msg: String, t: Throwable? = null) {\n    if (t == null)\n        Log.wtf(this::class.java.simpleName, msg)\n    else\n        Log.wtf(this::class.java.simpleName, msg, t)\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/SaveImage.kt",
    "content": "package io.github.drumber.kitsune.util\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.os.Build\nimport android.os.Environment\nimport android.provider.MediaStore\nimport java.util.*\n\nfun Context.saveImageInGallery(image: Bitmap, imageName: String? = null): Boolean {\n    val fileName = (if (!imageName.isNullOrBlank()) \"${imageName}_\" else \"\") + Date().formatDate(\"yyyy-MM-dd-HH-mm-ss\") + \".jpg\"\n\n    val contentValues = ContentValues().apply {\n        if (!imageName.isNullOrBlank()) put(MediaStore.MediaColumns.TITLE, imageName)\n        put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)\n        put(MediaStore.MediaColumns.MIME_TYPE, \"image/jpeg\")\n        put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000)\n        put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis())\n            put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)\n        }\n    }\n\n    val imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)\n    val outputStream = imageUri?.let { contentResolver.openOutputStream(it) }\n\n    outputStream?.use {\n        logD(\"Saving image to $imageUri\")\n        image.compress(Bitmap.CompressFormat.JPEG, 100, it)\n        return true\n    }\n    return false\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/TimeUtil.kt",
    "content": "package io.github.drumber.kitsune.util\n\nimport android.content.Context\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.util.extensions.format\nimport java.math.RoundingMode\nimport kotlin.math.roundToInt\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.minutes\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.DurationUnit\nimport kotlin.time.ExperimentalTime\n\nobject TimeUtil {\n\n    /**\n     * Formats the given time in seconds to a human readable time string like:\n     * `2 years, 1 month, 5 days, 1 hour, 2 minutes`\n     */\n    @OptIn(ExperimentalTime::class)\n    fun timeToHumanReadableFormat(\n        timeSeconds: Long,\n        context: Context,\n        includeSeconds: Boolean = false\n    ): String {\n        val res = context.resources\n        val parts = mutableListOf<String>()\n\n        var remaining = timeSeconds.seconds\n\n        val years = remaining.inWholeYears\n        if (years > 0) {\n            parts += res.getQuantityString(R.plurals.duration_years, years.toInt(), years)\n            remaining -= yearsToDuration(years)\n        }\n        val months = remaining.inWholeMonths\n        if (months > 0) {\n            parts += res.getQuantityString(R.plurals.duration_months, months.toInt(), months)\n            remaining -= monthsToDuration(months)\n        }\n        val days = remaining.inWholeDays\n        if (days > 0) {\n            parts += res.getQuantityString(R.plurals.duration_days, days.toInt(), days)\n            remaining -= days.days\n        }\n        val hours = remaining.inWholeHours\n        if (hours > 0) {\n            parts += res.getQuantityString(R.plurals.duration_hours, hours.toInt(), hours)\n            remaining -= hours.hours\n        }\n        val minutes = remaining.inWholeMinutes\n        if (minutes > 0) {\n            parts += res.getQuantityString(R.plurals.duration_minutes, minutes.toInt(), minutes)\n            remaining -= minutes.minutes\n        }\n        val seconds = remaining.inWholeSeconds\n        if (includeSeconds && seconds > 0) {\n            parts += res.getQuantityString(R.plurals.duration_seconds, seconds.toInt(), seconds)\n        }\n\n        return parts.joinToString(\", \")\n    }\n\n    /**\n     * Formats the given time in seconds to a rounded time string.\n     *\n     * Example:\n     * ```\n     * 1s    -> 1 second\n     * 59s   -> 59 seconds\n     * 60s   -> 1 minute\n     * 90s   -> 1.5 minutes\n     * 3600s -> 1 hour\n     * ```\n     */\n    @OptIn(ExperimentalTime::class)\n    fun roundTime(timeSeconds: Long, context: Context, decimalPlaces: Int = 1): String {\n        val res = context.resources\n        val time = timeSeconds.seconds\n\n        return when {\n            time.inWholeYears > 0 -> {\n                val years = time.toYearsDouble().round(decimalPlaces)\n                res.getQuantityString(R.plurals.duration_years, years.roundToInt(), years.format())\n            }\n            time.inWholeMonths > 0 -> {\n                val months = time.toMonthsDouble().round(decimalPlaces)\n                res.getQuantityString(R.plurals.duration_months, months.roundToInt(), months.format())\n            }\n            time.inWholeDays > 0 -> {\n                val days = time.toDouble(DurationUnit.DAYS).round(decimalPlaces)\n                res.getQuantityString(R.plurals.duration_days, days.roundToInt(), days.format())\n            }\n            time.inWholeHours > 0 -> {\n                val hours = time.toDouble(DurationUnit.HOURS).round(decimalPlaces)\n                res.getQuantityString(R.plurals.duration_hours, hours.roundToInt(), hours.format())\n            }\n            time.inWholeMinutes > 0 -> {\n                val minutes = time.toDouble(DurationUnit.MINUTES).round(decimalPlaces)\n                res.getQuantityString(R.plurals.duration_minutes, minutes.roundToInt(), minutes.format())\n            }\n            else -> {\n                res.getQuantityString(R.plurals.duration_seconds, timeSeconds.toInt(), timeSeconds)\n            }\n        }\n    }\n\n    @ExperimentalTime\n    private val Duration.inWholeMonths\n        get() = inWholeDays / 30\n\n    @ExperimentalTime\n    private val Duration.inWholeYears\n        get() = inWholeDays / 365\n\n    @ExperimentalTime\n    private fun monthsToDuration(value: Long) = (value * 30).days\n\n    @ExperimentalTime\n    private fun yearsToDuration(value: Long) = (value * 365).days\n\n    @ExperimentalTime\n    private fun Duration.toMonthsDouble() = this.toDouble(DurationUnit.DAYS) / 30.0\n\n    @ExperimentalTime\n    private fun Duration.toYearsDouble() = this.toDouble(DurationUnit.DAYS) / 365.0\n\n    private fun Double.round(decimalPlaces: Int) = this.toBigDecimal()\n        .setScale(decimalPlaces, RoundingMode.HALF_UP)\n        .toDouble()\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/extensions/ActivityExtensions.kt",
    "content": "package io.github.drumber.kitsune.util.extensions\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.content.res.Resources\nimport android.util.TypedValue\nimport android.widget.Toast\nimport androidx.annotation.ColorInt\nimport androidx.annotation.ColorRes\nimport androidx.core.content.ContextCompat\nimport androidx.core.view.WindowInsetsControllerCompat\nimport io.github.drumber.kitsune.R\n\nfun Activity.setStatusBarColor(@ColorInt color: Int) {\n    if (window.statusBarColor != color)\n        window.statusBarColor = color\n}\n\nfun Activity.setStatusBarColorRes(@ColorRes colorResource: Int) {\n    setStatusBarColor(ContextCompat.getColor(this, colorResource))\n}\n\nfun Activity.setLightStatusBar() {\n    WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true\n}\n\nfun Activity.clearLightStatusBar() {\n    WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = false\n}\n\nfun Activity.isLightStatusBar(): Boolean {\n    return WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars\n}\n\nfun Activity.clearLightNavigationBar() {\n    WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars = false\n}\n\nfun Context.isNightMode(): Boolean {\n    return (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES\n}\n\nfun Resources.Theme.getColor(resid: Int): Int {\n    val typedValue = TypedValue()\n    this.resolveAttribute(resid, typedValue, true)\n    return typedValue.data\n}\n\nfun Resources.Theme.getResourceId(resid: Int): Int {\n    val typedValue = TypedValue()\n    this.resolveAttribute(resid, typedValue, true)\n    return typedValue.resourceId\n}\n\nfun Context.showSomethingWrongToast() {\n    Toast.makeText(this, R.string.error_something_wrong, Toast.LENGTH_SHORT).show()\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/extensions/FragmentExtensions.kt",
    "content": "package io.github.drumber.kitsune.util.extensions\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.view.View\nimport androidx.annotation.IdRes\nimport androidx.core.app.ActivityOptionsCompat\nimport androidx.core.view.ViewCompat\nimport androidx.fragment.app.Fragment\nimport androidx.navigation.ActivityNavigatorExtras\nimport androidx.navigation.NavController\nimport androidx.navigation.NavDirections\nimport androidx.navigation.NavOptions\nimport androidx.navigation.Navigator\nimport androidx.navigation.fragment.findNavController\nimport io.github.drumber.kitsune.ui.photoview.PhotoViewActivityDirections\nimport io.github.drumber.kitsune.util.logE\n\n/**\n * Checks if the current destination of the back stack is equal to the specified destination id.\n * This avoids simultaneous navigation calls, e.g. when the user clicks on two list items at the same time.\n */\nfun NavController.navigateSafe(\n    @IdRes currentNavId: Int,\n    directions: NavDirections,\n    navOptions: NavOptions? = null\n) {\n    if (this.currentDestination?.id == currentNavId) {\n        this.navigate(directions, navOptions)\n    }\n}\n\n/**\n * Checks if the current destination of the back stack is equal to the specified destination id.\n * This avoids simultaneous navigation calls, e.g. when the user clicks on two list items at the same time.\n */\nfun NavController.navigateSafe(\n    @IdRes currentNavId: Int,\n    directions: NavDirections,\n    navigationExtras: Navigator.Extras\n) {\n    if (this.currentDestination?.id == currentNavId) {\n        this.navigate(directions, navigationExtras)\n    }\n}\n\nfun Fragment.showSomethingWrongToast() {\n    requireContext().showSomethingWrongToast()\n}\n\nfun Fragment.startUrlShareIntent(url: String, title: String? = null) {\n    val shareIntent = Intent.createChooser(Intent().apply {\n        action = Intent.ACTION_SEND\n        putExtra(Intent.EXTRA_TEXT, url)\n        type = \"text/plain\"\n    }, title)\n    startActivity(shareIntent)\n}\n\nfun Fragment.openUrl(url: String) {\n    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))\n    try {\n        startActivity(intent)\n    } catch (e: Exception) {\n        logE(\"Failed to open URL: $url\", e)\n    }\n}\n\nfun Fragment.openCharacterOnMAL(malId: Int) {\n    val malCharacterUrl = \"https://myanimelist.net/character/$malId\"\n    openUrl(malCharacterUrl)\n}\n\nfun Fragment.copyToClipboard(label: String, text: String) {\n    context?.copyToClipboard(label, text)\n}\n\nfun Fragment.openPhotoViewActivity(\n    imageUrl: String,\n    title: String? = null,\n    thumbnailUrl: String? = null,\n    sharedElement: View? = null\n) {\n    val transitionName = sharedElement?.let { ViewCompat.getTransitionName(it) }\n    val action = PhotoViewActivityDirections.actionGlobalPhotoViewActivity(\n        imageUrl,\n        title,\n        thumbnailUrl,\n        transitionName\n    )\n    val options = if (sharedElement != null && transitionName != null) {\n        ActivityOptionsCompat.makeSceneTransitionAnimation(\n            requireActivity(),\n            sharedElement,\n            transitionName\n        )\n    } else {\n        null\n    }\n    val extras = ActivityNavigatorExtras(options)\n    findNavController().navigate(action, extras)\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/extensions/OtherExtensions.kt",
    "content": "package io.github.drumber.kitsune.util.extensions\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.res.Resources\nimport androidx.core.view.get\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout\nimport androidx.viewpager2.widget.ViewPager2\nimport com.google.android.material.elevation.ElevationOverlayProvider\nimport io.github.drumber.kitsune.R\nimport java.text.NumberFormat\n\n/**\n * Make the internal RecyclerView of ViewPager2 accessible.\n */\nval ViewPager2.recyclerView: RecyclerView\n    get() = this[0] as RecyclerView\n\nfun SwipeRefreshLayout.setAppTheme() {\n    setProgressBackgroundColorSchemeColor(\n        ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(8.0f)\n    )\n    setColorSchemeColors(context.theme.getColor(R.attr.colorPrimary))\n}\n\nfun Context.copyToClipboard(label: String, text: String) {\n    val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager\n    val clip = ClipData.newPlainText(label, text)\n    clipboard?.setPrimaryClip(clip)\n}\n\n/**\n * Format double using default locale format.\n */\nfun Double.format(): String = NumberFormat.getInstance().format(this)\n\nfun Int.toDp() = (this / Resources.getSystem().displayMetrics.density).toInt()\n\nfun Int.toPx() = (this * Resources.getSystem().displayMetrics.density).toInt()\n\nfun Float.toPx() = this * Resources.getSystem().displayMetrics.density\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/json/AlgoliaFacetValueDeserializer.kt",
    "content": "package io.github.drumber.kitsune.util.json\n\nimport com.algolia.search.model.filter.Filter.Facet.Value\nimport com.fasterxml.jackson.core.JsonParseException\nimport com.fasterxml.jackson.core.JsonParser\nimport com.fasterxml.jackson.databind.DeserializationContext\nimport com.fasterxml.jackson.databind.JsonNode\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer\nimport com.fasterxml.jackson.databind.node.JsonNodeType\n\n/**\n * Custom jackson deserializer for [com.algolia.search.model.filter.Filter.Facet.Value].\n */\nclass AlgoliaFacetValueDeserializer @JvmOverloads constructor(vc: Class<*>? = null) :\n    StdDeserializer<Value>(vc) {\n\n    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Value {\n        val node = p.codec.readTree<JsonNode>(p)\n        return when (node.get(\"raw\").nodeType) {\n            JsonNodeType.STRING -> p.codec.treeToValue(node, Value.String::class.java)\n            JsonNodeType.BOOLEAN -> p.codec.treeToValue(node, Value.Boolean::class.java)\n            JsonNodeType.NUMBER -> p.codec.treeToValue(node, Value.Number::class.java)\n            else -> throw JsonParseException(\n                p,\n                \"Unsupported type of com.algolia.search.model.filter.Filter.Facet.Value 'raw' field.\"\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/json/AlgoliaNumericValueDeserializer.kt",
    "content": "package io.github.drumber.kitsune.util.json\n\nimport com.algolia.search.model.filter.Filter.Numeric.Value\nimport com.fasterxml.jackson.core.JsonParseException\nimport com.fasterxml.jackson.core.JsonParser\nimport com.fasterxml.jackson.databind.DeserializationContext\nimport com.fasterxml.jackson.databind.JsonNode\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer\n\n/**\n * Custom jackson deserializer for [com.algolia.search.model.filter.Filter.Numeric.Value].\n */\nclass AlgoliaNumericValueDeserializer @JvmOverloads constructor(vc: Class<*>? = null) :\n    StdDeserializer<Value>(vc) {\n\n    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Value {\n        val node = p.codec.readTree<JsonNode>(p)\n        return when {\n            isComparison(node) -> p.codec.treeToValue(node, Value.Comparison::class.java)\n            isRange(node) -> p.codec.treeToValue(node, Value.Range::class.java)\n            else -> throw JsonParseException(\n                p,\n                \"Unsupported type of com.algolia.search.model.filter.Filter.Numeric.Value 'raw' field.\"\n            )\n        }\n    }\n\n    private fun isComparison(node: JsonNode): Boolean {\n        return node.has(\"operator\") && node.has(\"number\")\n    }\n\n    private fun isRange(node: JsonNode): Boolean {\n        return node.has(\"lowerBound\") && node.has(\"upperBound\")\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/json/IgnoreParcelablePropertyMixin.kt",
    "content": "package io.github.drumber.kitsune.util.json\n\nimport com.fasterxml.jackson.annotation.JsonIgnore\n\nabstract class IgnoreParcelablePropertyMixin {\n    @JsonIgnore\n    abstract fun getStability(): Int\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/json/NullableIntSerializer.kt",
    "content": "package io.github.drumber.kitsune.util.json\n\nimport com.fasterxml.jackson.core.JsonGenerator\nimport com.fasterxml.jackson.databind.JsonSerializer\nimport com.fasterxml.jackson.databind.SerializerProvider\n\n/**\n * [JsonSerializer] that serializes `-1` to `null`.\n */\nclass NullableIntSerializer : JsonSerializer<Int?>() {\n\n    override fun serialize(value: Int?, gen: JsonGenerator, serializers: SerializerProvider) {\n        if (value != null && value == -1) {\n            gen.writeNull()\n        } else if (value != null) {\n            gen.writeNumber(value)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/network/AuthenticationInterceptor.kt",
    "content": "package io.github.drumber.kitsune.util.network\n\nimport io.github.drumber.kitsune.constants.Kitsu.API_HOST\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.domain.auth.RefreshAccessTokenUseCase\nimport io.github.drumber.kitsune.domain.auth.RefreshResult\nimport io.github.drumber.kitsune.util.logD\nimport io.github.drumber.kitsune.util.logI\nimport kotlinx.coroutines.runBlocking\nimport okhttp3.Authenticator\nimport okhttp3.Interceptor\nimport okhttp3.Request\nimport okhttp3.Response\nimport okhttp3.Route\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.get\n\ninterface AuthenticationInterceptor : Interceptor, Authenticator\n\nclass AuthenticationInterceptorImpl(\n    private val accessTokenRepository: AccessTokenRepository\n) : AuthenticationInterceptor, KoinComponent {\n\n    override fun intercept(chain: Interceptor.Chain): Response {\n        val requestBuilder = chain.request().newBuilder()\n        if (chain.request().url.host == API_HOST) {\n            accessTokenRepository.getAccessToken()?.accessToken?.let {\n                requestBuilder.header(\"Authorization\", \"Bearer $it\")\n            }\n        }\n        return chain.proceed(requestBuilder.build())\n    }\n\n    /**\n     * This method is automatically called by retrofit when a request fails with a 401 code.\n     */\n    override fun authenticate(route: Route?, response: Response): Request? {\n        if (response.request.url.host != API_HOST || response.responseCount > 3) return null\n\n        val localAccessTokenHolder = accessTokenRepository.getAccessToken()\n        val localAccessToken = localAccessTokenHolder?.accessToken\n        if (!accessTokenRepository.hasAccessToken() || localAccessToken == null) return null\n\n        // check if the token from the request differs from the local stored access token\n        val isTokenAlreadyRefreshed = response.request\n            .header(\"Authorization\")\n            ?.endsWith(localAccessToken) == false\n\n        val accessToken = if (isTokenAlreadyRefreshed) {\n            logD(\"Local access token was changed during this request. Do not refresh access token and retry with changed access token.\")\n            localAccessToken\n        } else {\n            logI(\"Refreshing access token because of a 401 Unauthorized response.\")\n            val refreshResult = runBlocking {\n                val refreshAccessToken: RefreshAccessTokenUseCase = get()\n                refreshAccessToken()\n            }\n            if (refreshResult !is RefreshResult.Success) return null\n            refreshResult.accessToken.accessToken\n        }\n\n        return response.request.newBuilder()\n            .header(\"Authorization\", \"Bearer $accessToken\")\n            .build()\n    }\n\n    private val Response.responseCount: Int\n        get() = generateSequence(this) { it.priorResponse }.count()\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/network/ResponseData.kt",
    "content": "package io.github.drumber.kitsune.util.network\n\nsealed class ResponseData<T>(open val data: T?) {\n\n    data class Success<T>(override val data: T): ResponseData<T>(data)\n    data class Error<T>(val e: Exception, override val data: T? = null): ResponseData<T>(data)\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/network/UserAgentInterceptor.kt",
    "content": "package io.github.drumber.kitsune.util.network\n\nimport okhttp3.Interceptor\nimport okhttp3.Response\n\nclass UserAgentInterceptor(private val userAgent: String) : Interceptor {\n\n    override fun intercept(chain: Interceptor.Chain): Response {\n        val request = chain.request().newBuilder()\n            .header(\"User-Agent\", userAgent)\n            .build()\n        return chain.proceed(request)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/rating/RatingFrequenciesUtil.kt",
    "content": "package io.github.drumber.kitsune.util.rating\n\nimport io.github.drumber.kitsune.data.common.media.RatingFrequencies\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\nimport io.github.drumber.kitsune.util.rating.RatingSystemUtil.convertFrom\n\nobject RatingFrequenciesUtil {\n\n    fun RatingFrequencies.transformToRatingSystem(ratingSystem: LocalRatingSystemPreference): List<Int> {\n        val ratingCounts = this.toList().map { it?.toIntOrNull() ?: 0 }\n\n        // contains the rating counts categorized by the rating type (1-4 for simple, 0.5-5 for regular, 1-10 for advanced)\n        val ratingsMap = mutableMapOf<String, Int>()\n\n        ratingCounts.forEachIndexed { index, value ->\n            val ratingValue = ratingSystem.convertFrom(index + 2).toString()\n            val prevValue = ratingsMap[ratingValue]\n            if (prevValue != null) {\n                ratingsMap[ratingValue] = prevValue + value\n            } else {\n                ratingsMap[ratingValue] = value\n            }\n        }\n\n        return ratingsMap.values.toList()\n    }\n\n    fun RatingFrequencies.calculateAverageRating(ratingSystem: LocalRatingSystemPreference): Double {\n        val ratingCounts = this.toList().map { it?.toIntOrNull() ?: 0 }\n        if (ratingCounts.isEmpty()) return 0.0\n\n        var sum = 0.0\n        var totalRatings = 0\n        for (i in ratingCounts.indices) {\n            val count = ratingCounts[i]\n\n            sum += count * ratingSystem.convertFrom(i + 2)\n            totalRatings += count\n        }\n\n        if (totalRatings == 0) return 0.0\n        return sum / totalRatings\n    }\n\n    fun RatingFrequencies.toList() = listOf(\n        r2,\n        r3,\n        r4,\n        r5,\n        r6,\n        r7,\n        r8,\n        r9,\n        r10,\n        r11,\n        r12,\n        r13,\n        r14,\n        r15,\n        r16,\n        r17,\n        r18,\n        r19,\n        r20\n    )\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/rating/RatingSystemUtil.kt",
    "content": "package io.github.drumber.kitsune.util.rating\n\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.math.floor\n\nobject RatingSystemUtil : KoinComponent {\n\n    private val DEFAULT = LocalRatingSystemPreference.Regular\n\n    private val userRepository: UserRepository by inject()\n\n    fun getRatingSystem(): LocalRatingSystemPreference {\n        return userRepository.localUser.value?.ratingSystem ?: DEFAULT\n    }\n\n    fun formatRating(ratingTwenty: Int, ratingSystem: LocalRatingSystemPreference = getRatingSystem()): String {\n        return ratingSystem.convertFrom(ratingTwenty).toString()\n    }\n\n    fun Int.formatRatingTwenty() = formatRating(this)\n\n    fun Int.fromRatingTwentyTo(ratingSystem: LocalRatingSystemPreference = getRatingSystem()) =\n        ratingSystem.convertFrom(this)\n\n    fun Float.toRatingTwentyFrom(ratingSystem: LocalRatingSystemPreference = getRatingSystem()) =\n        ratingSystem.convertToRatingTwenty(this)\n\n    fun LocalRatingSystemPreference.convertFrom(ratingTwenty: Int): Float {\n        return when (this) {\n            LocalRatingSystemPreference.Simple -> when (ratingTwenty) {\n                in 1..7 -> 1f\n                in 8..13 -> 2f\n                in 14..19 -> 3f\n                else -> 4f\n            }\n            LocalRatingSystemPreference.Regular -> floor(ratingTwenty / 2.0f) / 2.0f\n            LocalRatingSystemPreference.Advanced -> ratingTwenty / 2.0f\n        }\n    }\n\n    fun LocalRatingSystemPreference.convertToRatingTwenty(rating: Float): Int {\n        return when (this) {\n            LocalRatingSystemPreference.Simple -> when (rating) {\n                1f -> 2\n                2f -> 8\n                3f -> 14\n                else -> 20\n            }\n            LocalRatingSystemPreference.Regular -> floor(rating * 4).toInt()\n            LocalRatingSystemPreference.Advanced -> floor(rating * 2).toInt()\n        }\n    }\n\n    fun LocalRatingSystemPreference.stepSize(): Float {\n        return when (this) {\n            LocalRatingSystemPreference.Simple -> 1f\n            LocalRatingSystemPreference.Regular -> 0.5f\n            LocalRatingSystemPreference.Advanced -> 0.5f\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/ui/BindingAdapter.kt",
    "content": "package io.github.drumber.kitsune.util.ui\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.view.View\nimport android.widget.Button\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.appcompat.widget.TooltipCompat\nimport androidx.core.view.isVisible\nimport androidx.core.widget.doOnTextChanged\nimport androidx.databinding.BindingAdapter\nimport at.blogc.android.views.ExpandableTextView\nimport com.bumptech.glide.Glide\nimport com.google.android.material.button.MaterialButton\nimport io.github.drumber.kitsune.R\n\nobject BindingAdapter {\n\n    @JvmStatic\n    @BindingAdapter(\"actionButton\")\n    fun setExpandCollapseButton(expandableTextView: ExpandableTextView, actionView: TextView) {\n        expandableTextView.addOnExpandListener(object : ExpandableTextView.OnExpandListener {\n            override fun onExpand(view: ExpandableTextView) {\n                actionView.setText(R.string.action_read_less)\n            }\n            override fun onCollapse(view: ExpandableTextView) {\n                actionView.setText(R.string.action_read_more)\n            }\n        })\n        expandableTextView.post {\n            actionView.isVisible = expandableTextView.lineCount >= expandableTextView.maxLines\n        }\n        expandableTextView.doOnTextChanged { _, _, _, _ ->\n            expandableTextView.post {\n                actionView.isVisible = expandableTextView.lineCount >= expandableTextView.maxLines\n            }\n        }\n        actionView.setOnClickListener { expandableTextView.toggle() }\n    }\n\n    @JvmStatic\n    @BindingAdapter(\"isVisible\")\n    fun isVisible(view: View, isVisible: Boolean) {\n        view.isVisible = isVisible\n    }\n\n    @JvmStatic\n    @BindingAdapter(\"imageUrl\")\n    fun loadGlideImage(view: ImageView, url: String?) {\n        Glide.with(view)\n            .load(url)\n            .placeholder(R.drawable.ic_insert_photo_48)\n            .into(view)\n    }\n\n    @JvmStatic\n    @BindingAdapter(\"openOnClick\")\n    fun openUrl(view: View, url: String?) {\n        if (url.isNullOrBlank()) return\n        view.setOnClickListener {\n            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))\n            it.context.startActivity(intent)\n        }\n    }\n\n    @JvmStatic\n    @BindingAdapter(\"activated\")\n    fun isActivated(button: Button, isActivated: Boolean) {\n        button.isActivated = isActivated\n    }\n\n    @JvmStatic\n    @BindingAdapter(\"tooltip\")\n    fun tooltip(view: View, text: String) {\n        TooltipCompat.setTooltipText(view, text)\n    }\n\n    @JvmStatic\n    @BindingAdapter(\"iconPadding\")\n    fun iconPadding(button: MaterialButton, padding: Float) {\n        button.iconPadding = padding.toInt()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/ui/DateValidatorPointBetween.kt",
    "content": "package io.github.drumber.kitsune.util.ui\n\nimport com.google.android.material.datepicker.CalendarConstraints\nimport com.google.android.material.datepicker.DateValidatorPointBackward\nimport com.google.android.material.datepicker.DateValidatorPointForward\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class DateValidatorPointBetween(\n    private val pointBackward: DateValidatorPointBackward,\n    private val pointForward: DateValidatorPointForward\n) : CalendarConstraints.DateValidator {\n\n    companion object {\n        fun between(from: Long, before: Long) = DateValidatorPointBetween(\n            DateValidatorPointBackward.before(before),\n            DateValidatorPointForward.from(from)\n        )\n\n        fun nowAndFrom(from: Long) = DateValidatorPointBetween(\n            DateValidatorPointBackward.now(),\n            DateValidatorPointForward.from(from)\n        )\n    }\n\n    override fun isValid(date: Long): Boolean {\n        return pointBackward.isValid(date) && pointForward.isValid(date)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/ui/ProfileSiteLogo.kt",
    "content": "package io.github.drumber.kitsune.util.ui\n\nimport io.github.drumber.kitsune.R\n\nfun getProfileSiteLogoResourceId(name: String?): Int {\n    return when (name) {\n        \"Twitter\" -> R.drawable.ic_twitter\n        \"Facebook\" -> R.drawable.ic_facebook\n        \"YouTube\" -> R.drawable.ic_youtube\n        \"Google\" -> R.drawable.ic_google_plus\n        \"Instagram\" -> R.drawable.ic_instagram\n        \"Twitch\" -> R.drawable.ic_twitch\n        \"Vimeo\" -> R.drawable.ic_vimeo\n        \"GitHub\" -> R.drawable.ic_github\n        \"Battle.net\" -> R.drawable.ic_battle_net\n        \"Steam\" -> R.drawable.ic_steam\n        \"Raptr\" -> R.drawable.ic_raptr\n        \"Discord\" -> R.drawable.ic_discord\n        \"Tumblr\" -> R.drawable.ic_tumblr\n        \"SoundCloud\" -> R.drawable.ic_soundcloud\n        \"Dailymotion\" -> R.drawable.ic_dailymotion\n        \"Kickstarter\" -> R.drawable.ic_kickstarter\n        \"Mobcrush\" -> R.drawable.ic_mobcrush\n        \"osu!\" -> R.drawable.ic_osu\n        \"Patreon\" -> R.drawable.ic_patreon\n        \"DeviantArt\" -> R.drawable.ic_deviantart\n        \"Dribbble\" -> R.drawable.ic_dribbble\n        \"IMDb\" -> R.drawable.ic_imdb\n        \"Last.fm\" -> R.drawable.ic_lastfm\n        \"Letterboxd\" -> R.drawable.ic_letterboxd\n        \"Medium\" -> R.drawable.ic_medium\n        \"Player.me\" -> R.drawable.ic_player_me\n        \"Reddit\" -> R.drawable.ic_reddit\n        \"Trakt\" -> R.drawable.ic_trakt\n        \"Website\" -> R.drawable.ic_website\n        else -> R.drawable.ic_website\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/ui/RoundBitmapDrawable.kt",
    "content": "package io.github.drumber.kitsune.util.ui\n\nimport android.content.res.ColorStateList\nimport android.graphics.Bitmap\nimport android.graphics.BitmapShader\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.RectF\nimport android.graphics.Shader\nimport android.graphics.drawable.Drawable\nimport android.media.ThumbnailUtils\nimport io.github.drumber.kitsune.util.extensions.toPx\nimport kotlin.math.min\n\nclass RoundBitmapDrawable(private val bitmap: Bitmap) : Drawable() {\n\n    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)\n    private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG)\n    private val rect = RectF()\n    private var scaledBitmap: Bitmap? = null\n    private var tint: ColorStateList? = null\n    private val borderWidth = 1f.toPx()\n    private var isSelected = false\n\n    override fun draw(canvas: Canvas) {\n        val bounds = bounds\n        val width = bounds.width()\n        val height = bounds.height()\n        // use the smaller dimension as the circle size\n        val size = min(width, height)\n\n        // calculate the rectangle to center the bitmap\n        val left = (width - size) / 2f\n        val top = (height - size) / 2f\n        val right = left + size\n        val bottom = top + size\n\n        rect.set(left, top, right, bottom)\n\n        val tint = this.tint\n        if (tint != null) {\n            paint.color = tint.defaultColor\n        } else {\n            paint.color = Color.TRANSPARENT\n        }\n\n        // draw circular background\n        paint.shader = null\n        canvas.drawCircle(rect.centerX(), rect.centerY(), size / 2f, paint)\n\n        // draw the circular bitmap\n        val bitmap = scaledBitmap ?: updateScaledBitmap(bounds)\n        val bitmapShader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)\n        paint.shader = bitmapShader\n        canvas.drawCircle(rect.centerX(), rect.centerY(), size / 2f, paint)\n\n        if (isSelected && tint != null) {\n            // draw circular border when selected\n            borderPaint.color = tint.getColorForState(state, tint.defaultColor)\n            borderPaint.strokeWidth = borderWidth\n            borderPaint.style = Paint.Style.STROKE\n            val strokeRadius = (size - borderWidth) / 2f\n            canvas.drawCircle(rect.centerX(), rect.centerY(), strokeRadius, borderPaint)\n        }\n    }\n\n    override fun setTintList(tint: ColorStateList?) {\n        if (this.tint != tint) {\n            this.tint = tint\n        }\n    }\n\n    override fun setAlpha(alpha: Int) {\n        paint.alpha = alpha\n        borderPaint.alpha = alpha\n    }\n\n    override fun setColorFilter(colorFilter: ColorFilter?) {}\n\n    @Deprecated(\"Deprecated in Java\")\n    override fun getOpacity(): Int {\n        return PixelFormat.TRANSLUCENT\n    }\n\n    override fun onBoundsChange(bounds: Rect) {\n        super.onBoundsChange(bounds)\n        updateScaledBitmap(bounds)\n    }\n\n    override fun invalidateSelf() {\n        super.invalidateSelf()\n        scaledBitmap?.recycle()\n        scaledBitmap = null\n    }\n\n    override fun onStateChange(state: IntArray): Boolean {\n        val tint = tint ?: return false\n        val stateColor = tint.getColorForState(state, tint.defaultColor)\n        val isSelected = stateColor != tint.defaultColor &&\n                state.any { it == android.R.attr.state_selected }\n        if (this.isSelected != isSelected) {\n            this.isSelected = isSelected\n            invalidateSelf()\n            return true\n        }\n        return false\n    }\n\n    override fun isStateful(): Boolean {\n        return tint != null\n    }\n\n    private fun updateScaledBitmap(bounds: Rect): Bitmap {\n        scaledBitmap?.recycle()\n        scaledBitmap = null\n        // use the smaller dimension as the circle size\n        val size = min(bounds.width(), bounds.height())\n        val scaledBitmap = ThumbnailUtils.extractThumbnail(bitmap, size, size)\n        this.scaledBitmap = scaledBitmap\n        return scaledBitmap\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/ui/SnackbarUtils.kt",
    "content": "package io.github.drumber.kitsune.util.ui\n\nimport android.view.View\nimport androidx.annotation.StringRes\nimport com.google.android.material.snackbar.Snackbar\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateFailureReason.NotFound\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult.Failure\nimport io.github.drumber.kitsune.domain.library.LibraryEntryUpdateResult.Success\n\nfun showSnackbar(\n    parent: View,\n    message: CharSequence,\n    duration: Int = Snackbar.LENGTH_LONG\n): Snackbar {\n    return Snackbar.make(parent, message, duration).apply {\n        // fixes unnecessary bottom margin\n        view.initMarginWindowInsetsListener(left = true, right = true)\n        show()\n    }\n}\n\nfun showSnackbar(\n    parent: View,\n    @StringRes stringRes: Int,\n    duration: Int = Snackbar.LENGTH_LONG\n): Snackbar {\n    return showSnackbar(parent, parent.resources.getText(stringRes), duration)\n}\n\nfun LibraryEntryUpdateResult.showSnackbarOnFailure(parent: View): Snackbar? {\n    val stringRes = when (this) {\n        is Success -> return null\n        is Failure -> when (reason) {\n            NotFound -> R.string.error_library_update_not_found\n            else -> R.string.error_library_update_failed\n        }\n    }\n    return showSnackbar(parent, stringRes)\n}\n\nfun List<LibraryEntryUpdateResult>.showSnackbarOnAnyFailure(parent: View): Snackbar? {\n    val failedCount = count { it !is Success }\n    if (failedCount == 0) return null\n    val stringRes = when (failedCount) {\n        1 -> R.string.error_library_update_failed\n        else -> R.string.error_library_update_failed_multiple\n    }\n    return showSnackbar(parent, parent.resources.getString(stringRes, failedCount))\n}\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/util/ui/WindowInsetsUtil.kt",
    "content": "package io.github.drumber.kitsune.util.ui\n\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.core.view.marginBottom\nimport androidx.core.view.marginLeft\nimport androidx.core.view.marginRight\nimport androidx.core.view.marginTop\nimport androidx.core.view.updateLayoutParams\nimport androidx.core.view.updatePadding\nimport com.google.android.material.appbar.CollapsingToolbarLayout\n\nfun Toolbar.initWindowInsetsListener(consume: Boolean = true) {\n    val initialHeight = this.layoutParams.height\n    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->\n        val insets = windowInsets.getSystemBarsAndCutoutInsets()\n        view.updatePadding(\n            top = insets.top,\n            left = insets.left,\n            right = insets.right\n        )\n        view.layoutParams.height = initialHeight + insets.top\n        if (consume) WindowInsetsCompat.CONSUMED else windowInsets\n    }\n}\n\nfun CollapsingToolbarLayout.initWindowInsetsListener(consume: Boolean = true) {\n    val initialHeight = this.layoutParams.height\n    val defaultTitleMarginStart = this.expandedTitleMarginStart\n    val defaultTitleMarginEnd = this.expandedTitleMarginStart\n    val defaultScrimVisibleHeightTrigger = this.scrimVisibleHeightTrigger\n    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->\n        val insets = windowInsets.getSystemBarsAndCutoutInsets()\n        view.layoutParams.height = initialHeight + insets.top\n        this.scrimVisibleHeightTrigger = defaultScrimVisibleHeightTrigger + insets.top\n\n        val isRtl = ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL\n        this.expandedTitleMarginStart =\n            defaultTitleMarginStart + if (isRtl) insets.right else insets.left\n        this.expandedTitleMarginEnd =\n            defaultTitleMarginEnd + if (isRtl) insets.left else insets.right\n        if (consume) WindowInsetsCompat.CONSUMED else windowInsets\n    }\n}\n\nfun View.initPaddingWindowInsetsListener(\n    left: Boolean = false,\n    top: Boolean = false,\n    right: Boolean = false,\n    bottom: Boolean = false,\n    consume: Boolean = true\n) {\n    val (initialLeft, initialTop, initialRight, initialBottom) = listOf(\n        paddingLeft,\n        paddingTop,\n        paddingRight,\n        paddingBottom\n    )\n    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->\n        val insets = windowInsets.getSystemBarsAndCutoutInsets()\n        view.updatePadding(\n            left = if (left) insets.left + initialLeft else paddingLeft,\n            top = if (top) insets.top + initialTop else paddingTop,\n            right = if (right) insets.right + initialRight else paddingRight,\n            bottom = if (bottom) insets.bottom + initialBottom else paddingBottom\n        )\n        if (consume) WindowInsetsCompat.CONSUMED else windowInsets\n    }\n}\n\nfun View.initMarginWindowInsetsListener(\n    left: Boolean = false,\n    top: Boolean = false,\n    right: Boolean = false,\n    bottom: Boolean = false,\n    consume: Boolean = true\n) {\n    val (initialLeft, initialTop, initialRight, initialBottom) = listOf(\n        marginLeft,\n        marginTop,\n        marginRight,\n        marginBottom\n    )\n    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->\n        val insets = windowInsets.getSystemBarsAndCutoutInsets()\n        view.updateLayoutParams<ViewGroup.MarginLayoutParams> {\n            if (left) leftMargin = insets.left + initialLeft\n            if (top) topMargin = insets.top + initialTop\n            if (right) rightMargin = insets.right + initialRight\n            if (bottom) bottomMargin = insets.bottom + initialBottom\n        }\n        if (consume) WindowInsetsCompat.CONSUMED else windowInsets\n    }\n}\n\nfun View.initImePaddingWindowInsetsListener(subtractSystemBarInset: Boolean = true) {\n    val initialPaddingBottom = paddingBottom\n    ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->\n        val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())\n        val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom\n\n        val paddingBottom = if (subtractSystemBarInset) {\n            val systemBarsBottom = insets.getSystemBarsAndCutoutInsets().bottom\n            imeHeight - systemBarsBottom\n        } else {\n            imeHeight\n        }\n        if (imeVisible) {\n            view.updatePadding(bottom = initialPaddingBottom + paddingBottom)\n        } else {\n            view.updatePadding(bottom = initialPaddingBottom)\n        }\n        insets\n    }\n}\n\nfun View.initHeightWindowInsetsListener(\n    useBottomInset: Boolean = false,\n    consume: Boolean = true\n) {\n    val initialHeight = height\n    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->\n        val insets = windowInsets.getSystemBarsAndCutoutInsets()\n        view.updateLayoutParams {\n            height = initialHeight + if (useBottomInset) insets.bottom else insets.top\n        }\n        if (consume) WindowInsetsCompat.CONSUMED else windowInsets\n    }\n}\n\nfun WindowInsetsCompat.getSystemBarsAndCutoutInsets() = getInsets(\n    WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()\n)\n"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/work/SyncLibraryEntriesForWidgetWorker.kt",
    "content": "package io.github.drumber.kitsune.work\n\nimport android.content.Context\nimport androidx.work.CoroutineWorker\nimport androidx.work.WorkerParameters\nimport io.github.drumber.kitsune.constants.LibraryWidget\nimport io.github.drumber.kitsune.domain.library.FetchLibraryEntriesForWidgetUseCase\nimport io.github.drumber.kitsune.domain.library.SynchronizeLocalLibraryModificationsUseCase\nimport io.github.drumber.kitsune.preference.KitsunePref\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nclass SyncLibraryEntriesForWidgetWorker(\n    context: Context,\n    params: WorkerParameters\n) : CoroutineWorker(context, params), KoinComponent {\n\n    private val synchronizeLocalLibraryModifications: SynchronizeLocalLibraryModificationsUseCase by inject()\n    private val fetchLibraryEntriesForWidget: FetchLibraryEntriesForWidgetUseCase by inject()\n\n    override suspend fun doWork(): Result {\n        synchronizeLocalLibraryModifications()\n        fetchLibraryEntriesForWidget(LibraryWidget.MAX_ITEM_COUNT)\n        KitsunePref.lastLibraryFetchForWidget = System.currentTimeMillis()\n        return Result.success()\n    }\n\n    companion object {\n        const val TAG = \"syncLibraryEntriesForWidget\"\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/github/drumber/kitsune/work/UpdateLibraryWidgetWorker.kt",
    "content": "package io.github.drumber.kitsune.work\n\nimport android.content.Context\nimport androidx.glance.appwidget.updateAll\nimport androidx.work.CoroutineWorker\nimport androidx.work.WorkerParameters\nimport io.github.drumber.kitsune.ui.widget.LibraryAppWidget\n\nclass UpdateLibraryWidgetWorker(\n    private val context: Context,\n    params: WorkerParameters\n) : CoroutineWorker(context, params) {\n\n    override suspend fun doWork(): Result {\n        LibraryAppWidget().updateAll(context)\n        return Result.success()\n    }\n\n    companion object {\n        const val TAG = \"updateLibraryWidget\"\n    }\n}"
  },
  {
    "path": "app/src/main/res/anim/slide_down.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"@android:integer/config_shortAnimTime\">\n    <translate\n        android:interpolator=\"@android:anim/accelerate_interpolator\"\n        android:fromYDelta=\"0%p\"\n        android:toYDelta=\"100%p\" />\n\n    <alpha\n        android:fromAlpha=\"1.0\"\n        android:toAlpha=\"0.0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/slide_up.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"@android:integer/config_shortAnimTime\">\n    <translate\n        android:interpolator=\"@android:anim/accelerate_interpolator\"\n        android:fromYDelta=\"100%p\"\n        android:toYDelta=\"0%p\" />\n\n    <alpha\n        android:fromAlpha=\"0.0\"\n        android:toAlpha=\"1.0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/animator/scale_enter_anim.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"scaleX\"\n        android:valueFrom=\"0.8\"\n        android:valueTo=\"1.0\" />\n\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"scaleY\"\n        android:valueFrom=\"0.8\"\n        android:valueTo=\"1.0\" />\n\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"alpha\"\n        android:valueFrom=\"0.0\"\n        android:valueTo=\"1.0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/animator/scale_exit_anim.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"scaleX\"\n        android:valueFrom=\"1.0\"\n        android:valueTo=\"1.1\" />\n\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"scaleY\"\n        android:valueFrom=\"1.0\"\n        android:valueTo=\"1.1\" />\n\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"alpha\"\n        android:valueFrom=\"1.0\"\n        android:valueTo=\"0.0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/animator/scale_pop_enter_anim.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"scaleX\"\n        android:valueFrom=\"1.1\"\n        android:valueTo=\"1.0\" />\n\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"scaleY\"\n        android:valueFrom=\"1.1\"\n        android:valueTo=\"1.0\" />\n\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"alpha\"\n        android:valueFrom=\"0.0\"\n        android:valueTo=\"1.0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/animator/scale_pop_exit_anim.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"scaleX\"\n        android:valueFrom=\"1.0\"\n        android:valueTo=\"0.8\" />\n\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"scaleY\"\n        android:valueFrom=\"1.0\"\n        android:valueTo=\"0.8\" />\n\n    <objectAnimator\n        android:interpolator=\"?attr/motionEasingEmphasizedInterpolator\"\n        android:duration=\"?attr/motionDurationMedium1\"\n        android:propertyName=\"alpha\"\n        android:valueFrom=\"1.0\"\n        android:valueTo=\"0.0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/color/subtype_badge_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/black\" android:alpha=\"0.6\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/color/translucent_overlay.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"?attr/colorSurface\" android:alpha=\"0.7\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/color/translucent_status_bar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"?attr/colorSurface\" android:alpha=\"0.5\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/animated_favorite.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\">\n\n    <aapt:attr name=\"android:drawable\">\n        <vector\n            android:width=\"24dp\"\n            android:height=\"24dp\"\n            android:viewportWidth=\"24\"\n            android:viewportHeight=\"24\">\n            <group\n                android:name=\"wrapper\"\n                android:scaleX=\"1.0\"\n                android:scaleY=\"1.0\"\n                android:pivotX=\"12\"\n                android:pivotY=\"12\">\n                <path\n                    android:name=\"heart\"\n                    android:fillColor=\"?attr/colorControlNormal\"\n                    android:pathData=\"M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z\" />\n                <path\n                    android:name=\"red_heart\"\n                    android:fillColor=\"@color/color_heart\"\n                    android:pathData=\"M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z\" />\n            </group>\n        </vector>\n    </aapt:attr>\n\n    <target android:name=\"wrapper\">\n        <aapt:attr name=\"android:animation\">\n            <set>\n                <objectAnimator\n                    android:propertyName=\"scaleX\"\n                    android:duration=\"200\"\n                    android:valueFrom=\"1\"\n                    android:valueTo=\"0.9\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n                <objectAnimator\n                    android:propertyName=\"scaleY\"\n                    android:duration=\"200\"\n                    android:valueFrom=\"1\"\n                    android:valueTo=\"0.9\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n                <objectAnimator\n                    android:propertyName=\"scaleX\"\n                    android:startOffset=\"200\"\n                    android:duration=\"300\"\n                    android:valueFrom=\"0.9\"\n                    android:valueTo=\"1.1\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n                <objectAnimator\n                    android:propertyName=\"scaleY\"\n                    android:startOffset=\"200\"\n                    android:duration=\"300\"\n                    android:valueFrom=\"0.9\"\n                    android:valueTo=\"1.1\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n                <objectAnimator\n                    android:propertyName=\"scaleX\"\n                    android:startOffset=\"500\"\n                    android:duration=\"300\"\n                    android:valueFrom=\"1.1\"\n                    android:valueTo=\"1\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n                <objectAnimator\n                    android:propertyName=\"scaleY\"\n                    android:startOffset=\"500\"\n                    android:duration=\"300\"\n                    android:valueFrom=\"1.1\"\n                    android:valueTo=\"1\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n            </set>\n        </aapt:attr>\n    </target>\n\n    <target android:name=\"red_heart\">\n        <aapt:attr name=\"android:animation\">\n            <set>\n                <objectAnimator\n                    android:propertyName=\"fillAlpha\"\n                    android:duration=\"200\"\n                    android:valueFrom=\"0\"\n                    android:valueTo=\"1\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:anim/accelerate_interpolator\"/>\n                <objectAnimator\n                    android:propertyName=\"fillAlpha\"\n                    android:startOffset=\"700\"\n                    android:duration=\"300\"\n                    android:valueFrom=\"1\"\n                    android:valueTo=\"0\"\n                    android:valueType=\"floatType\"\n                    android:interpolator=\"@android:anim/accelerate_interpolator\"/>\n            </set>\n        </aapt:attr>\n    </target>\n</animated-vector>"
  },
  {
    "path": "app/src/main/res/drawable/badge_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <solid\n        android:color=\"?attr/colorSurfaceContainerHigh\" />\n\n    <corners\n        android:radius=\"25dp\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/bottom_edge_fade.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <gradient\n        android:angle=\"-90\"\n        android:startColor=\"@android:color/transparent\"\n        android:endColor=\"@color/edge_fade_color\"\n        android:type=\"linear\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/bottom_edge_fade_surface.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <gradient\n        android:angle=\"-90\"\n        android:startColor=\"@android:color/transparent\"\n        android:endColor=\"?attr/colorSurface\"\n        android:type=\"linear\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/cover_placeholder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <gradient\n        android:angle=\"-90\"\n        android:startColor=\"?attr/colorPlaceholderGradientStart\"\n        android:endColor=\"?attr/colorPlaceholderGradientEnd\"\n        android:type=\"linear\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/explore_section_divider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <size\n        android:height=\"20dp\"\n        android:width=\"0dp\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/ic_add_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_add_a_photo_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"960\" android:viewportWidth=\"960\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520Q440,520 440,520Q440,520 440,520L440,520Q440,520 440,520Q440,520 440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520L440,520ZM120,840Q87,840 63.5,816.5Q40,793 40,760L40,280Q40,247 63.5,223.5Q87,200 120,200L246,200L296,146Q307,134 322.5,127Q338,120 355,120L520,120Q537,120 548.5,131.5Q560,143 560,160L560,160Q560,177 548.5,188.5Q537,200 520,200L355,200L282,280L120,280Q120,280 120,280Q120,280 120,280L120,760Q120,760 120,760Q120,760 120,760L760,760Q760,760 760,760Q760,760 760,760L760,440Q760,423 771.5,411.5Q783,400 800,400L800,400Q817,400 828.5,411.5Q840,423 840,440L840,760Q840,793 816.5,816.5Q793,840 760,840L120,840ZM760,200L720,200Q703,200 691.5,188.5Q680,177 680,160Q680,143 691.5,131.5Q703,120 720,120L760,120L760,80Q760,63 771.5,51.5Q783,40 800,40Q817,40 828.5,51.5Q840,63 840,80L840,120L880,120Q897,120 908.5,131.5Q920,143 920,160Q920,177 908.5,188.5Q897,200 880,200L840,200L840,240Q840,257 828.5,268.5Q817,280 800,280Q783,280 771.5,268.5Q760,257 760,240L760,200ZM440,700Q515,700 567.5,647.5Q620,595 620,520Q620,445 567.5,392.5Q515,340 440,340Q365,340 312.5,392.5Q260,445 260,520Q260,595 312.5,647.5Q365,700 440,700ZM440,620Q398,620 369,591Q340,562 340,520Q340,478 369,449Q398,420 440,420Q482,420 511,449Q540,478 540,520Q540,562 511,591Q482,620 440,620Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_amazon.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"251.038\"\n    android:viewportHeight=\"259.969\">\n  <path\n      android:pathData=\"m219.336,209.886c-105.235,50.083 -170.545,8.18 -212.352,-17.271 -2.587,-1.604 -6.984,0.375 -3.169,4.757 13.928,16.888 59.573,57.593 119.153,57.593 59.621,0 95.09,-32.532 99.527,-38.207 4.407,-5.627 1.294,-8.731 -3.16,-6.872zM248.891,193.564c-2.826,-3.68 -17.184,-4.366 -26.22,-3.256 -9.05,1.078 -22.634,6.609 -21.453,9.93 0.606,1.244 1.843,0.686 8.06,0.127 6.234,-0.622 23.698,-2.826 27.337,1.931 3.656,4.79 -5.57,27.608 -7.255,31.288 -1.628,3.68 0.622,4.629 3.68,2.178 3.016,-2.45 8.476,-8.795 12.14,-17.774 3.639,-9.028 5.858,-21.622 3.71,-24.424z\"\n      android:fillColor=\"#00A8E1\"\n      android:fillType=\"nonZero\"/>\n  <path\n      android:pathData=\"m148.577,107.692c0,13.141 0.332,24.1 -6.31,35.77 -5.361,9.489 -13.853,15.324 -23.341,15.324 -12.952,0 -20.495,-9.868 -20.495,-24.432 0,-28.75 25.76,-33.968 50.146,-33.968zM182.592,189.908c-2.23,1.992 -5.456,2.135 -7.97,0.806 -11.196,-9.298 -13.189,-13.615 -19.356,-22.487 -18.502,18.882 -31.596,24.527 -55.601,24.527 -28.37,0 -50.478,-17.506 -50.478,-52.565 0,-27.373 14.85,-46.018 35.96,-55.126 18.313,-8.066 43.884,-9.489 63.43,-11.718v-4.365c0,-8.018 0.616,-17.506 -4.08,-24.432 -4.128,-6.215 -12.003,-8.777 -18.93,-8.777 -12.856,0 -24.337,6.594 -27.136,20.257 -0.57,3.037 -2.799,6.026 -5.835,6.168l-32.735,-3.51c-2.751,-0.618 -5.787,-2.847 -5.028,-7.07 7.543,-39.66 43.36,-51.616 75.43,-51.616 16.415,0 37.858,4.365 50.81,16.795 16.415,15.323 14.849,35.77 14.849,58.02v52.565c0,15.798 6.547,22.724 12.714,31.264 2.182,3.036 2.657,6.69 -0.095,8.966 -6.879,5.74 -19.119,16.415 -25.855,22.393l-0.095,-0.095\"\n      android:fillColor=\"#000\"\n      android:fillType=\"evenOdd\"/>\n  <path\n      android:pathData=\"m219.336,209.886c-105.235,50.083 -170.545,8.18 -212.352,-17.271 -2.587,-1.604 -6.984,0.375 -3.169,4.757 13.928,16.888 59.573,57.593 119.153,57.593 59.621,0 95.09,-32.532 99.527,-38.207 4.407,-5.627 1.294,-8.731 -3.16,-6.872zM248.891,193.564c-2.826,-3.68 -17.184,-4.366 -26.22,-3.256 -9.05,1.078 -22.634,6.609 -21.453,9.93 0.606,1.244 1.843,0.686 8.06,0.127 6.234,-0.622 23.698,-2.826 27.337,1.931 3.656,4.79 -5.57,27.608 -7.255,31.288 -1.628,3.68 0.622,4.629 3.68,2.178 3.016,-2.45 8.476,-8.795 12.14,-17.774 3.639,-9.028 5.858,-21.622 3.71,-24.424z\"\n      android:fillColor=\"#00A8E1\"\n      android:fillType=\"nonZero\"/>\n  <path\n      android:pathData=\"m148.577,107.692c0,13.141 0.332,24.1 -6.31,35.77 -5.361,9.489 -13.853,15.324 -23.341,15.324 -12.952,0 -20.495,-9.868 -20.495,-24.432 0,-28.75 25.76,-33.968 50.146,-33.968zM182.592,189.908c-2.23,1.992 -5.456,2.135 -7.97,0.806 -11.196,-9.298 -13.189,-13.615 -19.356,-22.487 -18.502,18.882 -31.596,24.527 -55.601,24.527 -28.37,0 -50.478,-17.506 -50.478,-52.565 0,-27.373 14.85,-46.018 35.96,-55.126 18.313,-8.066 43.884,-9.489 63.43,-11.718v-4.365c0,-8.018 0.616,-17.506 -4.08,-24.432 -4.128,-6.215 -12.003,-8.777 -18.93,-8.777 -12.856,0 -24.337,6.594 -27.136,20.257 -0.57,3.037 -2.799,6.026 -5.835,6.168l-32.735,-3.51c-2.751,-0.618 -5.787,-2.847 -5.028,-7.07 7.543,-39.66 43.36,-51.616 75.43,-51.616 16.415,0 37.858,4.365 50.81,16.795 16.415,15.323 14.849,35.77 14.849,58.02v52.565c0,15.798 6.547,22.724 12.714,31.264 2.182,3.036 2.657,6.69 -0.095,8.966 -6.879,5.74 -19.119,16.415 -25.855,22.393l-0.095,-0.095\"\n      android:fillColor=\"#000\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_animelab.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"44dp\"\n    android:height=\"50dp\"\n    android:viewportWidth=\"44\"\n    android:viewportHeight=\"50\">\n  <path\n      android:pathData=\"M21.6071,0.1056C21.7383,-0.0151 21.8695,0.1373 21.963,0.2262C22.2255,0.3507 22.5151,0.4321 22.7036,0.6674C22.9977,0.8258 23.2994,0.991 23.6169,1.1086C24.003,1.5302 24.638,1.5845 25.0173,2.0075C25.3582,2.083 25.638,2.3122 25.911,2.5219C26.1395,2.6131 26.356,2.7353 26.5686,2.8552C26.7587,2.8906 26.8793,3.0505 27.0317,3.1554C27.4057,3.3424 27.8084,3.4977 28.1169,3.7881C28.1938,3.8748 28.3379,3.8341 28.414,3.9223C28.6169,4.1222 28.9457,4.1229 29.1433,4.3288C29.4193,4.6229 29.8718,4.6463 30.1373,4.9593C30.4397,5.0897 30.7187,5.2692 31.0249,5.3906C31.1101,5.4638 31.1855,5.5483 31.2632,5.6297C31.5385,5.7074 31.779,5.8688 32.0354,5.9925C32.2511,6.0822 32.3884,6.2964 32.6192,6.3582C32.951,6.5452 33.3145,6.6652 33.5943,6.9299C33.8763,7.0611 34.1795,7.1697 34.368,7.4336C34.7941,7.589 35.2134,7.7956 35.5603,8.0943C35.8635,8.2059 36.1305,8.3959 36.4291,8.5158C36.7753,8.8258 37.23,8.9713 37.6259,9.2006C37.8296,9.4872 38.2164,9.4932 38.4683,9.7202C38.8635,10.0641 39.4261,10.1463 39.7964,10.5234C40.1275,10.6373 40.4095,10.862 40.7247,11.006C40.96,11.3198 41.4095,11.3032 41.6637,11.5897C42.0385,11.8869 42.5633,11.963 42.8846,12.3333C43.0716,12.3831 43.3296,12.4646 43.359,12.684C43.3567,20.9155 43.362,29.1478 43.356,37.3793C43.2662,37.4962 43.1471,37.6086 42.994,37.6305C42.8009,37.6614 42.7278,37.8922 42.5392,37.9404C42.181,38.1297 41.7775,38.2609 41.4955,38.5641C41.1991,38.6772 40.8959,38.7836 40.6818,39.0279C40.402,39.1275 40.1735,39.3311 39.8982,39.4374C39.7112,39.4834 39.6184,39.6848 39.4374,39.7436C39.0603,39.9314 38.6599,40.0882 38.3462,40.3771C38.0445,40.5008 37.7594,40.6538 37.4759,40.8062C37.267,41.0392 36.9646,41.1275 36.6968,41.27C36.598,41.3567 36.497,41.4412 36.3861,41.5113C36.0958,41.6109 35.8514,41.8062 35.5603,41.9057C35.2134,42.2044 34.7941,42.411 34.368,42.5664C34.1154,42.9367 33.6244,42.9646 33.3281,43.2828C33.0241,43.4193 32.7345,43.5988 32.4253,43.7315C32.0882,44.0211 31.6742,44.2134 31.2632,44.3703C30.9321,44.7866 30.3326,44.8303 29.957,45.1968C29.7187,45.3733 29.3967,45.4201 29.1833,45.635C28.9902,45.8688 28.6471,45.8673 28.4276,46.0664C28.3514,46.1652 28.1983,46.1237 28.1161,46.2127C27.721,46.6094 27.1252,46.6991 26.727,47.0905C26.448,47.19 26.1833,47.3341 25.96,47.5317C25.7481,47.6878 25.4661,47.7315 25.279,47.9238C25.0822,47.9419 24.9329,48.043 24.8069,48.1908C24.4336,48.4201 23.9781,48.5392 23.6621,48.8575C23.2655,49.0747 22.7866,49.1938 22.4759,49.5468C22.2391,49.6621 21.9691,49.7285 21.7866,49.9306C21.6463,49.9744 21.5686,49.8499 21.4751,49.7753C21.2134,49.6493 20.9223,49.5701 20.736,49.3326C20.4397,49.175 20.1388,49.0068 19.819,48.8937C19.4374,48.4676 18.7986,48.4178 18.4216,47.9917C18.0799,47.9186 17.8001,47.6885 17.5279,47.4781C17.2994,47.3869 17.0822,47.2655 16.8703,47.1455C16.6787,47.1101 16.5588,46.9487 16.4065,46.8446C16.0332,46.6576 15.6297,46.503 15.322,46.2119C15.2451,46.1244 15.0995,46.1667 15.0249,46.0784C14.822,45.8763 14.4917,45.8786 14.2949,45.6712C14.0196,45.3756 13.5656,45.3552 13.3017,45.0407C12.9985,44.9103 12.7195,44.7308 12.414,44.6094C12.3281,44.5354 12.2526,44.4517 12.1757,44.3703C11.8997,44.2926 11.6591,44.1312 11.4027,44.0075C11.187,43.9178 11.0498,43.7036 10.8198,43.6425C10.4887,43.4555 10.1259,43.3363 9.8469,43.0716C9.5649,42.9389 9.2594,42.8318 9.0709,42.5664C8.6448,42.411 8.2255,42.2044 7.8786,41.9057C7.5633,41.7783 7.27,41.6003 6.9668,41.4532C6.6833,41.1561 6.236,41.1169 5.9623,40.8062C5.6682,40.6388 5.3552,40.4992 5.052,40.3462C4.6463,39.954 4.04,39.8741 3.6425,39.4766C3.3107,39.3635 3.0302,39.138 2.7149,38.9947C2.4789,38.6795 2.0287,38.6976 1.7753,38.4103C1.3967,38.1116 0.871,38.0339 0.5483,37.6621C0.356,37.635 0.2066,37.5264 0.0739,37.3906C0.0882,32.0415 0.0762,26.6916 0.0799,21.3424C0.0762,18.4314 0.0882,15.5196 0.0747,12.6086C0.2081,12.4751 0.3575,12.3658 0.5483,12.3379C0.8725,11.9668 1.3974,11.8884 1.7753,11.5905C2.0302,11.3047 2.4781,11.319 2.7149,11.006C3.0309,10.8627 3.3115,10.638 3.6425,10.5234C3.9774,10.1878 4.46,10.0814 4.8454,9.8205C5.2293,9.4646 5.7919,9.3989 6.1606,9.0234C6.3333,8.9035 6.5392,8.8416 6.7247,8.7436C6.871,8.6176 7.0181,8.4811 7.2089,8.4291C7.4299,8.313 7.6433,8.181 7.8786,8.0943C8.2255,7.7956 8.6456,7.5897 9.0709,7.4336C9.4261,6.9781 10.0716,6.9351 10.4615,6.5287C10.7413,6.3952 11.037,6.2896 11.2632,6.0709C11.5626,5.9155 11.8522,5.73 12.1757,5.6297C12.2919,5.506 12.4012,5.3627 12.5769,5.3258C12.813,5.1931 13.0573,5.0762 13.3017,4.9593C13.5671,4.6463 14.0189,4.6229 14.2956,4.3288C14.5068,4.1124 14.8605,4.1161 15.0649,3.8861C15.1689,3.8484 15.2911,3.8462 15.365,3.7519C15.7564,3.3816 16.3281,3.2896 16.7127,2.9103C17.0008,2.819 17.2398,2.6214 17.5271,2.5219C17.8001,2.3122 18.0799,2.0822 18.4223,2.0075C18.7345,1.6531 19.227,1.5611 19.6048,1.2971C19.9774,0.9268 20.5769,0.8922 20.9268,0.4872C21.1373,0.3356 21.4314,0.31 21.6071,0.1056L21.6071,0.1056ZM21.5551,17.0799C21.4449,17.4012 21.2722,17.6953 21.0566,17.9563C20.7489,18.4721 20.4284,18.9789 20.0709,19.4615C19.6199,20.3167 19.0151,21.0777 18.5241,21.9087C18.2843,22.4057 17.8899,22.8069 17.6403,23.2994C17.3454,23.6938 17.0732,24.1033 16.8545,24.546C16.0686,25.6614 15.3854,26.8446 14.6305,27.9796C14.3303,28.3296 14.1192,28.7398 13.905,29.144C13.5905,29.5618 13.3235,30.0121 13.0294,30.4434C12.8906,30.7443 12.6184,30.9548 12.4985,31.2677C12.2594,31.7934 11.8115,32.2119 11.6418,32.7655C12.2881,32.8801 12.8039,32.3944 13.3989,32.2504C14.1214,32.0083 14.822,31.7081 15.5385,31.4495C16.1908,31.1342 16.9351,31.0588 17.5611,30.6855C17.859,30.5581 18.1802,30.4962 18.4842,30.3891C19.003,30.0762 19.635,30.0452 20.1523,29.73C20.3446,29.6275 20.5633,29.5897 20.7662,29.5113C21.0799,29.4419 21.3507,29.2587 21.6538,29.1591C21.8431,29.129 22.0038,29.2617 22.1742,29.322C22.5256,29.5091 22.9306,29.5588 23.2881,29.7308C23.8047,30.0452 24.4359,30.0769 24.9555,30.3891C25.3077,30.5173 25.6938,30.5701 26.0219,30.7624C26.4548,31.0015 26.9472,31.0867 27.4072,31.2549C28.3786,31.6207 29.3356,32.0241 30.325,32.3416C30.7964,32.537 31.2617,32.8605 31.7979,32.7655C31.7662,32.5528 31.6282,32.3808 31.5211,32.2006C31.2391,31.8137 31.006,31.3952 30.7805,30.9744C30.4653,30.6018 30.2451,30.1621 29.9615,29.7655C29.764,29.4321 29.497,29.1448 29.3416,28.7866C29.1554,28.3703 28.8175,28.049 28.6018,27.6523C27.9125,26.6259 27.3001,25.552 26.5837,24.5452C26.3718,24.1207 26.1176,23.7217 25.8288,23.3454C25.6161,22.9653 25.3643,22.6094 25.1071,22.2579C24.5845,21.2707 23.8643,20.405 23.3394,19.4186C22.8899,18.8484 22.5701,18.1923 22.132,17.6154C22.0347,17.4434 21.951,17.2647 21.8839,17.0799C21.7738,17.0799 21.6644,17.0807 21.5551,17.0799L21.5551,17.0799Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#3E307C\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_back_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_drop_down_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M7,10l5,5 5,-5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_forward_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_bar_chart_16.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"16dp\" android:tint=\"?attr/colorControlNormal\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"16dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M6,20L6,20c1.1,0 2,-0.9 2,-2v-7c0,-1.1 -0.9,-2 -2,-2h0c-1.1,0 -2,0.9 -2,2v7C4,19.1 4.9,20 6,20z\"/>\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M16,15v3c0,1.1 0.9,2 2,2h0c1.1,0 2,-0.9 2,-2v-3c0,-1.1 -0.9,-2 -2,-2h0C16.9,13 16,13.9 16,15z\"/>\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,20L12,20c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h0c-1.1,0 -2,0.9 -2,2v12C10,19.1 10.9,20 12,20z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_battle_net.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"32\"\n    android:viewportWidth=\"32\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorOnSurface\" android:pathData=\"M26.578,14.34C26.578,14.34 28.883,14.457 28.883,13.102C28.883,11.332 25.813,9.738 25.813,9.738C25.813,9.738 26.293,8.719 26.594,8.148C26.895,7.578 27.738,5.359 27.813,4.852C27.906,4.211 27.762,4.012 27.762,4.012C27.555,5.379 25.328,9.316 25.148,9.449C22.977,8.434 19.992,8.148 19.992,8.148C19.992,8.148 17.07,2 14.32,2C11.594,2 11.609,7.266 11.609,7.266C11.609,7.266 10.84,5.773 9.871,5.773C8.457,5.773 7.992,7.906 7.992,10.223C5.203,10.223 2.855,10.848 2.645,10.906C2.438,10.965 1.777,11.445 2.074,11.387C2.688,11.191 5.555,10.746 8.063,10.965C8.203,13.164 9.488,16.031 9.488,16.031C9.488,16.031 6.73,20.023 6.73,22.871C6.73,23.621 7.059,24.992 9.035,24.992C10.695,24.992 12.559,23.996 12.906,23.797C12.602,24.23 12.375,25.063 12.375,25.445C12.375,25.758 12.563,26.645 13.84,26.645C15.48,26.645 17.316,25.387 17.316,25.387C17.316,25.387 19.051,28.262 20.531,29.578C20.93,29.934 21.313,30 21.313,30C21.313,30 19.84,28.586 17.902,24.938C19.703,23.828 21.578,21.203 21.578,21.203C21.578,21.203 21.801,21.211 23.512,21.211C26.191,21.211 29.996,20.648 29.996,18.52C30,16.324 26.578,14.34 26.578,14.34ZM26.875,13.016C26.875,13.793 26.137,13.785 26.137,13.785L25.574,13.82C25.574,13.82 24.508,13.262 23.859,12.996C23.859,12.996 24.863,11.453 25.098,11.023C25.273,11.129 26.875,12.129 26.875,13.016ZM15.66,5.098C16.922,5.098 18.719,8.066 18.719,8.066C18.719,8.066 15.914,7.816 13.605,9.172C13.668,7.035 14.387,5.098 15.66,5.098ZM10.672,7.504C11.07,7.504 11.461,7.992 11.625,8.402C11.625,8.676 11.766,10.27 11.766,10.27L9.453,10.18C9.453,8.098 10.27,7.504 10.672,7.504ZM10.43,21.977C9.164,21.977 8.906,21.273 8.906,20.641C8.906,19.207 10.051,17.199 10.051,17.199C10.051,17.199 11.336,19.898 13.574,21.035C12.465,21.688 11.547,21.977 10.43,21.977ZM14.535,24.801C13.648,24.801 13.539,24.227 13.539,24.094C13.539,23.684 13.863,23.195 13.863,23.195C13.863,23.195 15.352,22.191 15.445,22.082L16.547,24.137C16.547,24.137 15.422,24.801 14.535,24.801ZM17.301,23.684C16.762,22.742 16.363,21.758 16.363,21.758C16.363,21.758 18.578,21.898 19.77,20.672C19.027,21.004 17.844,21.426 16.469,21.297C19.344,18.766 21.023,16.93 22.441,15.035C22.32,14.887 21.672,14.434 21.512,14.359C20.656,15.391 17.324,18.949 14.238,20.711C10.332,18.582 9.512,12.32 9.43,11.02L11.563,11.223C11.563,11.223 10.762,12.645 10.762,13.691C10.762,14.734 10.887,14.789 10.887,14.789C10.887,14.789 10.859,12.969 11.984,11.563C12.844,16.125 13.738,18.461 14.434,19.855C14.789,19.707 15.449,19.414 15.449,19.414C15.449,19.414 13.48,13.738 13.59,9.898C14.484,9.422 15.809,8.93 17.301,8.93C21.23,8.93 24.391,10.617 24.391,10.617L23.156,12.344C23.156,12.344 22.055,10.352 20.496,9.996C21.316,10.605 22.238,11.414 22.715,12.574C19.457,11.305 15.527,10.633 14.266,10.484C14.156,10.949 14.172,11.613 14.172,11.613C14.172,11.613 19.441,12.586 23.277,14.777C23.25,19.574 18.023,23.258 17.301,23.684ZM22.293,20.098C22.293,20.098 23.93,17.953 23.902,15.109C23.902,15.109 26.547,16.746 26.547,18.344C26.547,20.125 22.293,20.098 22.293,20.098Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_bookmark_added_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19,21l-7,-3l-7,3V5c0,-1.1 0.9,-2 2,-2l7,0c-0.63,0.84 -1,1.87 -1,3c0,2.76 2.24,5 5,5c0.34,0 0.68,-0.03 1,-0.1V21zM17.83,9L15,6.17l1.41,-1.41l1.41,1.41l3.54,-3.54l1.41,1.41L17.83,9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_cake_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,6c1.11,0 2,-0.9 2,-2 0,-0.38 -0.1,-0.73 -0.29,-1.03L12,0l-1.71,2.97c-0.19,0.3 -0.29,0.65 -0.29,1.03 0,1.1 0.9,2 2,2zM16.6,15.99l-1.07,-1.07 -1.08,1.07c-1.3,1.3 -3.58,1.31 -4.89,0l-1.07,-1.07 -1.09,1.07C6.75,16.64 5.88,17 4.96,17c-0.73,0 -1.4,-0.23 -1.96,-0.61L3,21c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1v-4.61c-0.56,0.38 -1.23,0.61 -1.96,0.61 -0.92,0 -1.79,-0.36 -2.44,-1.01zM18,9h-5L13,7h-2v2L6,9c-1.66,0 -3,1.34 -3,3v1.54c0,1.08 0.88,1.96 1.96,1.96 0.52,0 1.02,-0.2 1.38,-0.57l2.14,-2.13 2.13,2.13c0.74,0.74 2.03,0.74 2.77,0l2.14,-2.13 2.13,2.13c0.37,0.37 0.86,0.57 1.38,0.57 1.08,0 1.96,-0.88 1.96,-1.96L20.99,12C21,10.34 19.66,9 18,9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_calendar_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,3h-1L19,1h-2v2L7,3L7,1L5,1v2L4,3c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,21L4,21L4,8h16v13z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_calendar_month_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_cancel_presentation_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M21,19.1H3V5h18v14.1zM21,3H3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M14.59,8L12,10.59 9.41,8 8,9.41 10.59,12 8,14.59 9.41,16 12,13.41 14.59,16 16,14.59 13.41,12 16,9.41z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_check_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_close_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_cloud_off_16.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_code_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_contv.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"145dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"290\"\n    android:viewportHeight=\"80\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M26.8,2.6a39.8,39.8 0,0 0,-2.7 74c8.5,4 26.6,4 34.7,0 9,-4.4 9,-4 4.6,-10.4a49.7,49.7 0,0 1,-5.1 -9.9c-1.7,-5 -3.1,-6.9 -4.2,-5.2 -5,7.6 -22.6,5.7 -27,-3 -7.8,-15.3 7.2,-29.3 23,-21.5 3.3,1.6 6.3,2.8 6.4,2.6l2.1,-5.7c1,-2.9 3.3,-7.5 5.3,-10.3 2,-2.8 3.3,-5.3 3.1,-5.5 -7.4,-6.6 -29.5,-9.4 -40.2,-5.1m57.4,1.2C45.4,23.2 57.8,77.7 101.4,79c44.4,1.5 59.5,-55.5 20,-75.3C115,0.7 113.6,0.4 102.6,0.4S90.4,0.7 84.2,3.8m65.2,36v39.5h24.4l0.3,-14.5 0.3,-14.5 7.8,5.8c4.2,3.2 12.6,9.7 18.5,14.5l10.8,8.7H235V49.5l5,-0.4 5,-0.4L249,56l8,15.3c4.1,8 6,9.7 7.4,6 0.4,-1 6.5,-12.4 13.5,-25.3 15.9,-29.3 15.9,-27.3 0.2,-26.9l-12.5,0.3 -1.5,3.2 -1.5,3.2 -2,-3.5 -2.2,-3.4h-45l0.3,-12.2 0.4,-12.3H187l0.3,14.3 0.3,14.3 -6.6,-5a956,956 0,0 1,-18 -14.3c-6.3,-5.1 -12,-9.3 -12.6,-9.3 -0.7,0 -1.1,13.9 -1.1,39.5m-38,-13.2c7.7,3.7 10,15 4.5,22 -9,11.5 -29.8,5.4 -29.8,-8.5C86,28 99.3,21 111.3,26.7m127.5,10.4l2.9,5.6h-13.5l0.3,15.5 0.3,15.6h-11.1l0.4,-15.6 0.3,-15.5h-10.7l0.4,-5.6 0.4,-5.6H236l2.8,5.6m19.7,1.7c1.8,4 3.5,7.2 3.8,7.2 0.2,0 1.9,-3.3 3.7,-7.2l3.3,-7.3h12.4l-2.7,4.8 -10,18.4c-6,11.5 -7.3,13.4 -8,11.3 -0.4,-1.4 -4.6,-9.7 -9.4,-18.5l-8.7,-16h12.2l3.4,7.3\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_crunchyroll.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"50dp\"\n    android:height=\"50dp\"\n    android:viewportWidth=\"50\"\n    android:viewportHeight=\"50\">\n  <path\n      android:pathData=\"M22.5489,49.1452C21.7343,49.0678 19.5912,48.689 18.7962,48.482C11.9235,46.6921 6.1032,41.8918 3.0231,35.4731C1.3346,31.9542 0.6309,28.8073 0.6336,24.7878C0.6364,20.7633 1.3509,17.5534 3.0121,14.1025C4.2563,11.5178 5.6874,9.493 7.7191,7.4428C11.5195,3.6078 16.3334,1.2338 21.7861,0.5055C23.5687,0.2674 27.3423,0.3449 29.0072,0.6538C32.4697,1.2962 35.5784,2.5579 38.3643,4.4514C44.152,8.3855 47.9061,14.402 48.8847,21.3125C49.0943,22.792 49.2156,25.8709 49.0744,26.1281C48.9975,26.2684 48.9575,26.1214 48.907,25.5132C48.6572,22.4983 47.3786,18.8527 45.6145,16.1254C40.2527,7.836 30.2489,4.3191 20.9867,7.4674C13.8377,9.8975 8.4647,16.0626 6.99,23.5277C6.2608,27.219 6.4797,30.8371 7.6481,34.4091C9.7087,40.7085 14.6404,45.7411 20.8947,47.9265C22.3697,48.442 24.2639,48.8714 25.5131,48.9737C27.0088,49.0961 26.6318,49.2125 24.7864,49.1981C23.7795,49.1903 22.7726,49.1664 22.5489,49.1452L22.5489,49.1452Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#F78B24\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M27.6851,46.0996C19.9539,45.5253 13.5483,39.6454 12.2115,31.8962C11.9682,30.4857 11.9213,27.849 12.1164,26.5514C13.2755,18.8453 19.0868,12.9991 26.668,11.9124C28.2046,11.6922 30.9428,11.7694 32.4144,12.0744C33.6945,12.3398 35.1136,12.8108 36.2284,13.3404L37.0929,13.7511L36.2793,14.1431C33.3431,15.5574 31.5308,18.8657 31.9562,22.0348C32.3822,25.2084 34.5339,27.6994 37.6227,28.5948C38.7347,28.9172 40.4351,28.9171 41.5476,28.5946C42.986,28.1776 44.1141,27.4956 45.1408,26.4224C45.4873,26.06 45.7932,25.8008 45.8205,25.8462C45.8478,25.8917 45.927,26.3913 45.9966,26.9564C46.1681,28.3505 46.0674,31.003 45.7926,32.3274C44.9174,36.5451 42.7121,40.0853 39.3305,42.701C36.1298,45.1768 31.8958,46.4124 27.6851,46.0996L27.6851,46.0996Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#F78B24\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_dailymotion.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"48\"\n    android:viewportWidth=\"48\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#0066DC\" android:fillType=\"evenOdd\"\n        android:pathData=\"M0,48L48,48L48,0L0,0L0,48ZM41.391,41.565L34.236,41.565L34.236,38.76C32.038,40.911 29.794,41.706 26.801,41.706C23.762,41.706 21.143,40.724 18.945,38.76C16.046,36.188 14.549,32.821 14.549,28.893C14.549,25.292 15.952,22.066 18.571,19.541C20.909,17.249 23.762,16.08 26.941,16.08C29.981,16.08 32.319,17.109 34.002,19.26L34.002,8.318L41.391,6.787L41.391,41.565ZM28.157,22.627C24.65,22.627 21.938,25.479 21.938,28.846C21.938,32.353 24.65,35.019 28.438,35.019C31.618,35.019 34.283,32.4 34.283,28.94C34.283,25.339 31.618,22.627 28.157,22.627Z\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_delete_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_deviantart.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"100\"\n    android:viewportWidth=\"100\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#00CC76\" android:pathData=\"M78.06,22.94V4.9c0,-0.54 -0.44,-0.99 -0.99,-0.99H61.54c-0.26,0 -0.51,0.1 -0.69,0.29l-1.66,1.65c-0.07,0.07 -0.13,0.15 -0.18,0.24l-7.77,14.82l-1.83,1.06H22.93c-0.54,0 -0.99,0.44 -0.99,0.99v22.53c0,0.54 0.44,0.99 0.99,0.99h13.88l0.69,0.69l-15.44,29.48c-0.07,0.14 -0.11,0.3 -0.11,0.46v18c-0.01,0.53 0.46,1 0.99,0.99l15.54,-0.01c0.26,0 0.51,-0.1 0.69,-0.29c0.17,-0.17 1.79,-1.74 1.84,-1.89c0,0 7.78,-14.83 7.78,-14.83l1.78,-1.03h26.5c0.54,0 0.99,-0.44 0.99,-0.99V54.53c0,-0.54 -0.44,-0.99 -0.99,-0.99H63.14l-0.65,-0.65l15.45,-29.5C78.02,23.26 78.06,23.1 78.06,22.94z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_discord.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"256\"\n    android:viewportWidth=\"256\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#5865F2\" android:fillType=\"nonZero\" android:pathData=\"M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_done_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_dribbble.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"100\"\n    android:viewportWidth=\"100\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#D43C89\" android:pathData=\"M2.5,50c2.62,63.36 92.41,63.31 95,0C94.9,-13.32 5.11,-13.34 2.5,50z\"/>\n    <path android:fillColor=\"#B2005F\" android:pathData=\"M62.47,48.7c14.26,-1.79 28.41,1.07 29.82,1.38c-0.07,-10.14 -3.7,-19.46 -9.65,-26.76c-0.91,1.23 -8.09,10.46 -23.91,16.93C60.09,43.01 61.33,45.86 62.47,48.7z\"/>\n    <path android:fillColor=\"#B2005F\" android:pathData=\"M78.38,18.62C70.83,11.88 60.89,7.79 49.99,7.79c-3.45,0.02 -6.82,0.45 -10.03,1.21c1.18,1.59 8.96,12.22 15.98,25.13C71.18,28.41 77.62,19.72 78.38,18.62z\"/>\n    <path android:fillColor=\"#B2005F\" android:pathData=\"M48.33,36.73c-7.09,-12.65 -14.75,-23.28 -15.89,-24.84C20.47,17.56 11.49,28.64 8.73,41.96C10.64,41.98 28.29,42.04 48.33,36.73z\"/>\n    <path android:fillColor=\"#B2005F\" android:pathData=\"M53.32,50.34c0.55,-0.18 1.11,-0.35 1.68,-0.51c-1.06,-2.42 -2.23,-4.86 -3.45,-7.23c-21.21,6.36 -41.83,6.1 -43.69,6.08c-0.38,11.28 3.84,21.98 10.91,29.79C19.74,76.8 31.4,57.44 53.32,50.34z\"/>\n    <path android:fillColor=\"#B2005F\" android:pathData=\"M57.36,56.94c-23.4,8.18 -31.81,24.45 -32.55,25.97c10.98,9 28.45,11.48 41.65,5.47c-0.63,-3.66 -3.02,-16.36 -8.85,-31.53C57.53,56.9 57.42,56.92 57.36,56.94z\"/>\n    <path android:fillColor=\"#B2005F\" android:pathData=\"M65.84,54.85c5.44,15.02 7.65,27.23 8.11,29.77c9.32,-6.35 15.98,-16.36 17.84,-27.99C90.35,56.17 78.89,52.76 65.84,54.85z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_edit_24.xml",
    "content": "<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,17.46v3.04c0,0.28 0.22,0.5 0.5,0.5h3.04c0.13,0 0.26,-0.05 0.35,-0.15L17.81,9.94l-3.75,-3.75L3.15,17.1c-0.1,0.1 -0.15,0.22 -0.15,0.36zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_emoji_events_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19,5h-2V3H7v2H5C3.9,5 3,5.9 3,7v1c0,2.55 1.92,4.63 4.39,4.94c0.63,1.5 1.98,2.63 3.61,2.96V19H7v2h10v-2h-4v-3.1c1.63,-0.33 2.98,-1.46 3.61,-2.96C19.08,12.63 21,10.55 21,8V7C21,5.9 20.1,5 19,5zM5,8V7h2v3.82C5.84,10.4 5,9.3 5,8zM19,8c0,1.3 -0.84,2.4 -2,2.82V7h2V8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_facebook.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"16\"\n    android:viewportWidth=\"16\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#1877F2\" android:pathData=\"M15,8a7,7 0,0 0,-7 -7,7 7,0 0,0 -1.094,13.915v-4.892H5.13V8h1.777V6.458c0,-1.754 1.045,-2.724 2.644,-2.724 0.766,0 1.567,0.137 1.567,0.137v1.723h-0.883c-0.87,0 -1.14,0.54 -1.14,1.093V8h1.941l-0.31,2.023H9.094v4.892A7.001,7.001 0,0 0,15 8z\"/>\n    <path android:fillColor=\"#ffffff\" android:pathData=\"M10.725,10.023L11.035,8H9.094V6.687c0,-0.553 0.27,-1.093 1.14,-1.093h0.883V3.87s-0.801,-0.137 -1.567,-0.137c-1.6,0 -2.644,0.97 -2.644,2.724V8H5.13v2.023h1.777v4.892a7.037,7.037 0,0 0,2.188 0v-4.892h1.63z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_favorite_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_favorite_border_24.xml",
    "content": "<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=\"M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_filter_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_funimation.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"50dp\"\n    android:height=\"50dp\"\n    android:viewportWidth=\"50\"\n    android:viewportHeight=\"50\">\n  <path\n      android:pathData=\"M24.0655,0.0171C28.6912,-0.1562 33.3573,0.991 37.368,3.3034C40.4408,5.0637 43.1275,7.491 45.2011,10.3609C47.2603,13.2074 48.7074,16.4937 49.4084,19.9363C50.2271,23.9372 50.0494,28.1373 48.8896,32.053C47.9129,35.3492 46.2442,38.439 44.0215,41.0619C41.8896,43.5808 39.2603,45.6822 36.3178,47.1795C33.0619,48.8582 29.4264,49.7882 25.7657,49.8977C21.6553,50.0377 17.5099,49.1481 13.833,47.3007C8.018,44.4291 3.4255,39.1939 1.3438,33.0503C-0.8187,26.7935 -0.3546,19.6759 2.6463,13.7711C5.4829,8.0691 10.5943,3.5494 16.6023,1.4354C18.9973,0.5835 21.5251,0.1068 24.0655,0.0171L24.0655,0.0171ZM15.6508,33.3276C16.1149,35.6113 17.5898,37.6849 19.6418,38.807C21.8151,40.0242 24.4057,40.2513 26.8429,39.9883C28.8447,39.7711 30.8294,38.9955 32.298,37.5907C33.4713,36.4399 34.3151,34.9426 34.6275,33.3241C33.439,33.2971 32.2505,33.3241 31.0619,33.2935C30.4937,33.3761 29.9246,33.246 29.3573,33.3079C28.1248,33.3205 26.8923,33.3115 25.6598,33.2989C25.0054,33.3645 24.351,33.263 23.6966,33.3115C22.5305,33.2576 21.3627,33.3537 20.1966,33.2864C18.6822,33.3653 17.1661,33.2513 15.6508,33.3276L15.6508,33.3276Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#411299\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_github.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,0.297c-6.63,0 -12,5.373 -12,12 0,5.303 3.438,9.8 8.205,11.385 0.6,0.113 0.82,-0.258 0.82,-0.577 0,-0.285 -0.01,-1.04 -0.015,-2.04 -3.338,0.724 -4.042,-1.61 -4.042,-1.61C4.422,18.07 3.633,17.7 3.633,17.7c-1.087,-0.744 0.084,-0.729 0.084,-0.729 1.205,0.084 1.838,1.236 1.838,1.236 1.07,1.835 2.809,1.305 3.495,0.998 0.108,-0.776 0.417,-1.305 0.76,-1.605 -2.665,-0.3 -5.466,-1.332 -5.466,-5.93 0,-1.31 0.465,-2.38 1.235,-3.22 -0.135,-0.303 -0.54,-1.523 0.105,-3.176 0,0 1.005,-0.322 3.3,1.23 0.96,-0.267 1.98,-0.399 3,-0.405 1.02,0.006 2.04,0.138 3,0.405 2.28,-1.552 3.285,-1.23 3.285,-1.23 0.645,1.653 0.24,2.873 0.12,3.176 0.765,0.84 1.23,1.91 1.23,3.22 0,4.61 -2.805,5.625 -5.475,5.92 0.42,0.36 0.81,1.096 0.81,2.22 0,1.606 -0.015,2.896 -0.015,3.286 0,0.315 0.21,0.69 0.825,0.57C20.565,22.092 24,17.592 24,12.297c0,-6.627 -5.373,-12 -12,-12\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_google_plus.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"20\"\n    android:viewportWidth=\"20\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorOnSurface\" android:fillType=\"evenOdd\"\n        android:pathData=\"M18,8.707L18,6.826L16,6.826L16,8.707L14,8.707L14,10.587L16,10.587L16,12.468L18,12.468L18,10.587L20,10.587L20,8.707L18,8.707ZM12.4,8.707C12.454,8.707 12.5,9.526 12.5,9.92C12.5,13.347 10.058,16 6.377,16C2.852,16 0,13.316 0,10.001C0,6.686 2.852,4 6.377,4C8.099,4 9.539,4.581 10.65,5.558L8.919,7.106C8.446,6.678 7.616,6.143 6.377,6.143C4.2,6.143 2.423,7.761 2.423,9.851C2.423,11.941 4.2,13.483 6.377,13.483C8.901,13.483 9.849,11.527 9.994,10.587L6,10.587L6,8.707L12.4,8.707Z\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_heart_broken_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M16.5,3c-0.96,0 -1.9,0.25 -2.73,0.69L12,9h3l-3,10l1,-9h-3l1.54,-5.39C10.47,3.61 9.01,3 7.5,3C4.42,3 2,5.42 2,8.5c0,4.13 4.16,7.18 10,12.5c5.47,-4.94 10,-8.26 10,-12.5C22,5.42 19.58,3 16.5,3z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_hidive.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"50dp\"\n    android:height=\"50dp\"\n    android:viewportWidth=\"50\"\n    android:viewportHeight=\"50\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M22.614,28.231a0.966,0.956 0,1 0,1.932 0a0.966,0.956 0,1 0,-1.932 0z\"/>\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M25.46,28.231a0.966,0.956 0,1 0,1.932 0a0.966,0.956 0,1 0,-1.932 0z\"/>\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M25.003,16.76a10.293,10.178 0,0 1,16.47 -2.65,19.93 19.707,0 0,0 -32.94,0 10.293,10.178 0,0 1,16.47 2.65z\"/>\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M50,21.319a5.488,5.427 0,0 0,-6.967 -5.237,10.293 10.178,0 0,1 -8.431,15.406 10.263,10.148 0,0 1,0.696 3.678c0,0.293 -0.014,0.584 -0.04,0.87h-20.51a10.229,10.229 0,0 1,-0.04 -0.87,10.263 10.148,0 0,1 0.696,-3.678 10.293,10.178 0,0 1,-8.431 -15.406A5.498,5.436 0,1 0,5.13 26.743l0.472,7.183 0.864,-1.45 2.45,7.038 0.397,-2.133 5.604,6.876 -0.334,-2.244A19.819,19.597 0,0 0,20.9 44.5a10.312,10.197 0,0 1,-5.954 -7.177h20.118a10.312,10.197 0,0 1,-5.955 7.177,19.819 19.597,0 0,0 6.318,-2.487l-0.334,2.244 5.603,-6.876 0.397,2.134 2.45,-7.037 0.861,1.45 0.474,-7.183A5.5,5.438 0,0 0,50 21.32z\"/>\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M36.804,26.064a5.01,5.01 0,0 0,1.158 -0.97,5.245 5.245,0 0,0 0.581,-0.795 5.39,5.39 0,0 0,0.415 -0.883c0.056,-0.152 0.104,-0.308 0.143,-0.465a5.047,5.047 0,0 0,0.15 -0.96,4.915 4.86,0 0,0 0,-0.486 4.848,4.793 0,0 0,-0.044 -0.487,4.869 4.814,0 0,0 -0.239,-0.959 4.856,4.801 0,0 0,-0.445 -0.915,4.763 4.71,0 0,0 -0.914,-1.062 5.033,5.033 0,0 0,-1.157 -0.74,4.954 4.954,0 0,0 -1.762,-0.437 4.515,4.464 0,0 0,-0.458 0c-0.15,0.006 -0.304,0.02 -0.459,0.042a4.604,4.552 0,0 0,-0.453 0.088,4.434 4.384,0 0,0 -0.445,0.134 4.537,4.486 0,0 0,-0.437 0.183,4.6 4.55,0 0,0 -0.423,0.232 4.391,4.342 0,0 0,-0.362 0.25,7.564 7.564,0 0,0 -0.334,0.282 4.257,4.209 0,0 0,-0.297 0.308,5.47 5.47,0 0,0 -0.265,0.33 4.53,4.48 0,0 0,-0.704 1.519,4.446 4.396,0 0,0 -0.123,0.826c-0.008,0.138 -0.008,0.28 0,0.418 0.008,0.139 0.02,0.282 0.04,0.418a4.17,4.124 0,0 0,0.384 1.223,4.369 4.32,0 0,0 0.223,0.387 4.053,4.007 0,0 0,0.496 0.626,4.128 4.082,0 0,0 0.6,0.51 4.169,4.122 0,0 0,1.413 0.634,4.094 4.048,0 0,0 0.77,0.11 3.918,3.874 0,0 0,0.779 -0.038,3.86 3.816,0 0,0 0.766,-0.196 3.737,3.695 0,0 0,0.37 -0.16,3.816 3.773,0 0,0 0.358,-0.201 3.69,3.648 0,0 0,0.576 -0.452,3.754 3.712,0 0,0 0.467,-0.544 3.792,3.75 0,0 0,0.352 -0.614,3.841 3.798,0 0,0 0.23,-0.66 3.694,3.653 0,0 0,0.098 -0.694,3.602 3.562,0 0,0 -0.04,-0.703 3.49,3.45 0,0 0,-0.524 -1.347,3.351 3.314,0 0,0 -0.426,-0.523 3.426,3.388 0,0 0,-0.5 -0.415,3.51 3.471,0 0,0 -0.565,-0.312 3.417,3.379 0,0 0,-0.61 -0.201,3.38 3.342,0 0,0 -0.637,-0.085 3.149,3.113 0,0 0,-1.869 0.518,3.032 2.998,0 0,0 -0.46,0.373 3.085,3.05 0,0 0,-0.374 0.446,3.134 3.1,0 0,0 -0.28,0.502 3.092,3.057 0,0 0,-0.178 0.541,2.972 2.938,0 0,0 -0.072,0.565 2.847,2.815 0,0 0,0.04 0.57,2.784 2.752,0 0,0 0.16,0.559 2.847,2.815 0,0 0,0.287 0.53,2.727 2.696,0 0,0 0.74,0.721 2.759,2.728 0,0 0,0.939 0.393,2.672 2.642,0 0,0 0.506,0.059 2.465,2.437 0,0 0,1.481 -0.44,2.325 2.299,0 0,0 0.347,-0.297 2.408,2.381 0,0 0,0.616 -1.166,2.282 2.257,0 0,0 0.046,-0.44 2.168,2.144 0,0 0,-0.042 -0.44,2.127 2.103,0 0,0 -0.372,-0.833 2.064,2.041 0,0 0,-1.279 -0.791,2.084 2.06,0 0,0 -0.38,-0.032 1.864,1.843 0,0 0,-0.379 0.044,1.795 1.775,0 0,0 -0.715,0.33 1.67,1.651 0,0 0,-0.236 0.22,1.765 1.745,0 0,0 -0.187,0.259 1.727,1.708 0,0 0,-0.215 0.592,1.608 1.59,0 0,0 -0.017,0.315 1.46,1.443 0,0 0,0.36 0.895,1.348 1.333,0 0,0 0.183,0.176 1.366,1.35 0,0 0,0.445 0.234,1.36 1.344,0 0,0 0.247,0.053 1.208,1.195 0,0 0,0.26 0,1.236 1.222,0 0,0 0.265,-0.052 1.186,1.172 0,0 0,0.251 -0.11,1.113 1.1,0 0,0 0.223,-0.177 1.041,1.03 0,0 0,0.213 -0.314,1.06 1.048,0 0,0 0.084,-0.372 0.875,0.865 0,0 0,-0.016 -0.207,0.853 0.843,0 0,0 -0.07,-0.21c-0.164,-0.33 -0.407,-0.356 -0.445,-0.346 -0.013,0 -0.012,0.032 -0.01,0.044 0.035,0.257 0.041,0.52 -0.142,0.69a0.583,0.577 0,0 1,-0.511 0.143,0.46 0.456,0 0,1 -0.153,-0.05 0.47,0.465 0,0 1,-0.131 -0.102,0.539 0.533,0 0,1 -0.112,-0.231 0.668,0.66 0,0 1,0 -0.263,0.81 0.8,0 0,1 0.1,-0.259 0.78,0.77 0,0 1,0.187 -0.211,0.845 0.836,0 0,1 0.172,-0.104 0.913,0.903 0,0 1,0.187 -0.06,0.982 0.971,0 0,1 0.196,-0.019 1.041,1.03 0,0 1,0.195 0.033,1.14 1.127,0 0,1 0.542,0.272 1.093,1.081 0,0 1,0.14 0.154,1.156 1.143,0 0,1 0.134,0.232 1.215,1.201 0,0 1,0.077 0.25,1.32 1.306,0 0,1 0.022,0.26 1.407,1.392 0,0 1,-0.024 0.263,1.49 1.473,0 0,1 -0.205,0.49 1.483,1.466 0,0 1,-0.17 0.21,1.42 1.404,0 0,1 -0.209,0.179 1.539,1.521 0,0 1,-0.298 0.162,1.602 1.584,0 0,1 -0.647,0.11 1.747,1.727 0,0 1,-0.328 -0.041,1.794 1.774,0 0,1 -0.318,-0.101 1.844,1.823 0,0 1,-0.295 -0.158,1.781 1.761,0 0,1 -0.263,-0.21 1.737,1.717 0,0 1,-0.222 -0.26,1.862 1.84,0 0,1 -0.299,-0.739 1.994,1.972 0,0 1,-0.025 -0.39,2.092 2.069,0 0,1 0.052,-0.39 2.16,2.136 0,0 1,0.125 -0.374,2.208 2.183,0 0,1 0.194,-0.35 2.152,2.128 0,0 1,0.258 -0.309,2.087 2.063,0 0,1 0.318,-0.257 2.166,2.141 0,0 1,0.426 -0.22,2.227 2.202,0 0,1 0.45,-0.12 2.358,2.332 0,0 1,0.463 -0.027,2.45 2.422,0 0,1 0.46,0.063 2.52,2.491 0,0 1,1.213 0.674,2.425 2.398,0 0,1 0.302,0.37 2.523,2.495 0,0 1,0.248 0.486,2.588 2.559,0 0,1 0.136,0.512 2.71,2.68 0,0 1,0.028 0.525,2.851 2.82,0 0,1 -0.249,1.019 2.851,2.82 0,0 1,-0.614 0.87,2.811 2.78,0 0,1 -0.431,0.337 2.895,2.862 0,0 1,-0.557 0.273,2.937 2.904,0 0,1 -0.587,0.148 3.109,3.074 0,0 1,-0.597 0.027,3.167 3.131,0 0,1 -0.593,-0.086 3.259,3.222 0,0 1,-1.092 -0.492,3.197 3.16,0 0,1 -0.46,-0.39 3.13,3.095 0,0 1,-0.383 -0.483,3.229 3.193,0 0,1 -0.469,-1.26 3.404,3.366 0,0 1,-0.03 -0.66,3.52 3.48,0 0,1 0.1,-0.653 3.563,3.523 0,0 1,0.223 -0.624,3.619 3.578,0 0,1 0.334,-0.576 3.53,3.49 0,0 1,0.446 -0.507,3.545 3.505,0 0,1 1.227,-0.748 3.623,3.582 0,0 1,0.722 -0.176,3.816 3.773,0 0,1 0.735,-0.03 3.911,3.868 0,0 1,0.727 0.11,3.973 3.928,0 0,1 0.696,0.244 3.921,3.877 0,0 1,1.206 0.854,3.786 3.743,0 0,1 0.465,0.597 3.897,3.853 0,0 1,0.362 0.744,3.99 3.945,0 0,1 0.193,0.782 4.17,4.123 0,0 1,0.03 0.796,4.293 4.245,0 0,1 -0.126,0.787 4.375,4.326 0,0 1,-0.27 0.752,4.319 4.27,0 0,1 -0.41,0.691 4.275,4.228 0,0 1,-0.54 0.61,4.096 4.05,0 0,1 -0.315,0.264 3.93,3.885 0,0 1,-0.347 0.236,4.154 4.108,0 0,1 -0.402,0.22 4.24,4.193 0,0 1,-0.415 0.17,4.321 4.273,0 0,1 -0.426,0.124 4.378,4.329 0,0 1,-0.434 0.08,4.332 4.284,0 0,1 -0.436,0.036 4.258,4.21 0,0 1,-0.438 -0.007,4.508 4.508,0 0,1 -0.861,-0.135 4.96,4.96 0,0 1,-0.824 -0.293,4.649 4.649,0 0,1 -1.105,-0.715 4.582,4.53 0,0 1,-0.61 -0.647,4.505 4.454,0 0,1 -0.257,-0.372 4.716,4.664 0,0 1,-0.235 -0.43,4.646 4.594,0 0,1 -0.185,-0.44 4.745,4.692 0,0 1,-0.135 -0.457,4.97 4.97,0 0,1 -0.085,-0.462 4.329,4.329 0,0 1,-0.038 -0.467c-0.005,-0.158 0,-0.311 0.008,-0.467a4.895,4.895 0,0 1,0.288 -1.366,4.25 4.25,0 0,1 0.182,-0.433 4.85,4.85 0,0 1,0.784 -1.177c0.104,-0.116 0.222,-0.235 0.333,-0.34a4.774,4.72 0,0 1,0.373 -0.31,4.884 4.83,0 0,1 0.407,-0.274 5.001,4.945 0,0 1,0.469 -0.248,5.08 5.024,0 0,1 0.483,-0.195 4.951,4.896 0,0 1,0.495 -0.142,5.021 4.965,0 0,1 0.502,-0.09 5.04,5.04 0,0 1,1.015 -0.032,5.12 5.12,0 0,1 0.999,0.16 5.811,5.811 0,0 1,0.954 0.34,5.645 5.645,0 0,1 0.872,0.514 5.427,5.427 0,0 1,0.769 0.678,5.26 5.2,0 0,1 0.631,0.83c0.058,0.095 0.111,0.193 0.166,0.292a0.051,0.05 0,0 0,0.033 0.025,0.053 0.053,0 0,0 0.043,-0.006 0.051,0.05 0,0 0,0.019 -0.063,5.5 5.438,0 1,0 -7.515,6.998 5.441,5.38 0,0 0,2.462 0.58,5.51 5.449,0 0,0 1.705,-0.268c0.125,-0.046 0.271,-0.102 0.414,-0.17a5.122,5.064 0,0 0,0.483 -0.258zM18.4,26.064a5.01,5.01 0,0 0,1.157 -0.97,5.245 5.245,0 0,0 0.582,-0.795c0.085,-0.143 0.16,-0.287 0.228,-0.433 0.068,-0.145 0.131,-0.298 0.187,-0.45a4.45,4.45 0,0 0,0.142 -0.465,5.047 5.047,0 0,0 0.15,-0.96 4.915,4.86 0,0 0,0 -0.486,4.848 4.793,0 0,0 -0.044,-0.487 4.869,4.814 0,0 0,-0.238 -0.959,4.856 4.801,0 0,0 -0.445,-0.915 4.763,4.71 0,0 0,-0.915 -1.062,5.033 5.033,0 0,0 -1.157,-0.74 4.954,4.954 0,0 0,-1.762 -0.437,4.515 4.464,0 0,0 -0.458,0c-0.149,0.006 -0.304,0.02 -0.458,0.042a4.604,4.552 0,0 0,-0.453 0.088,4.434 4.384,0 0,0 -0.446,0.134 4.537,4.486 0,0 0,-0.436 0.183,4.6 4.55,0 0,0 -0.423,0.232 4.391,4.342 0,0 0,-0.362 0.25,7.564 7.564,0 0,0 -0.334,0.282 4.257,4.209 0,0 0,-0.297 0.308,5.47 5.47,0 0,0 -0.265,0.33 4.53,4.48 0,0 0,-0.704 1.519,4.446 4.396,0 0,0 -0.124,0.826c-0.008,0.138 -0.008,0.28 0,0.418 0.008,0.139 0.02,0.282 0.04,0.418a4.17,4.124 0,0 0,0.384 1.223,4.369 4.32,0 0,0 0.223,0.387 4.053,4.007 0,0 0,0.497 0.626,4.128 4.082,0 0,0 0.599,0.51 4.169,4.122 0,0 0,1.414 0.634,4.094 4.048,0 0,0 0.77,0.11 3.918,3.874 0,0 0,0.779 -0.038,3.86 3.816,0 0,0 0.766,-0.196 3.737,3.695 0,0 0,0.37 -0.16,3.816 3.773,0 0,0 0.358,-0.201 3.69,3.648 0,0 0,0.575 -0.452,3.754 3.712,0 0,0 0.468,-0.544 3.792,3.75 0,0 0,0.352 -0.614,3.841 3.798,0 0,0 0.23,-0.66 3.694,3.653 0,0 0,0.097 -0.694,3.602 3.562,0 0,0 -0.04,-0.703 3.49,3.45 0,0 0,-0.523 -1.347,3.351 3.314,0 0,0 -0.417,-0.513 3.426,3.388 0,0 0,-0.5 -0.415,3.51 3.471,0 0,0 -0.564,-0.312 3.417,3.379 0,0 0,-0.61 -0.201,3.38 3.342,0 0,0 -0.637,-0.085 3.149,3.113 0,0 0,-1.878 0.508,3.032 2.998,0 0,0 -0.464,0.374 3.085,3.05 0,0 0,-0.373 0.446,3.134 3.1,0 0,0 -0.279,0.502 3.092,3.057 0,0 0,-0.178 0.541,2.972 2.938,0 0,0 -0.073,0.565 2.847,2.815 0,0 0,0.04 0.57,2.784 2.752,0 0,0 0.16,0.559 2.847,2.815 0,0 0,0.287 0.53,2.727 2.696,0 0,0 0.74,0.721 2.759,2.728 0,0 0,0.939 0.393,2.672 2.642,0 0,0 0.507,0.059 2.465,2.437 0,0 0,1.48 -0.44,2.325 2.299,0 0,0 0.348,-0.297 2.408,2.381 0,0 0,0.616 -1.166,2.282 2.257,0 0,0 0.045,-0.44 2.168,2.144 0,0 0,-0.042 -0.44,2.127 2.103,0 0,0 -0.372,-0.833 2.064,2.041 0,0 0,-1.278 -0.791,2.084 2.06,0 0,0 -0.38,-0.032 1.864,1.843 0,0 0,-0.38 0.044,1.795 1.775,0 0,0 -0.714,0.33 1.67,1.651 0,0 0,-0.236 0.22,1.765 1.745,0 0,0 -0.187,0.26 1.727,1.708 0,0 0,-0.215 0.591,1.608 1.59,0 0,0 -0.018,0.315 1.46,1.443 0,0 0,0.36 0.895,1.348 1.333,0 0,0 0.183,0.176 1.366,1.35 0,0 0,0.446 0.234,1.36 1.344,0 0,0 0.247,0.053 1.208,1.195 0,0 0,0.26 0,1.236 1.222,0 0,0 0.264,-0.052 1.186,1.172 0,0 0,0.252 -0.11,1.113 1.1,0 0,0 0.222,-0.177 1.041,1.03 0,0 0,0.213 -0.314,1.06 1.048,0 0,0 0.085,-0.372 0.875,0.865 0,0 0,-0.017 -0.207,0.853 0.843,0 0,0 -0.069,-0.21c-0.165,-0.33 -0.407,-0.356 -0.445,-0.346 -0.014,0 -0.013,0.032 -0.01,0.044 0.034,0.257 0.04,0.52 -0.143,0.69a0.583,0.577 0,0 1,-0.51 0.143,0.46 0.456,0 0,1 -0.153,-0.05 0.47,0.465 0,0 1,-0.132 -0.102,0.539 0.533,0 0,1 -0.111,-0.23 0.668,0.66 0,0 1,0 -0.264,0.81 0.8,0 0,1 0.099,-0.259 0.78,0.77 0,0 1,0.187 -0.211,0.845 0.836,0 0,1 0.173,-0.104 0.913,0.903 0,0 1,0.187 -0.06,0.982 0.971,0 0,1 0.196,-0.019 1.041,1.03 0,0 1,0.199 0.02,1.14 1.127,0 0,1 0.542,0.272 1.093,1.081 0,0 1,0.14 0.154,1.156 1.143,0 0,1 0.127,0.247 1.215,1.201 0,0 1,0.077 0.25,1.32 1.306,0 0,1 0.022,0.258 1.407,1.392 0,0 1,-0.03 0.261,1.49 1.473,0 0,1 -0.205,0.489 1.483,1.466 0,0 1,-0.169 0.211,1.42 1.404,0 0,1 -0.21,0.179 1.539,1.521 0,0 1,-0.297 0.162,1.602 1.584,0 0,1 -0.647,0.11 1.747,1.727 0,0 1,-0.329 -0.041,1.794 1.774,0 0,1 -0.317,-0.101 1.844,1.823 0,0 1,-0.295 -0.158,1.781 1.761,0 0,1 -0.263,-0.21 1.737,1.717 0,0 1,-0.223 -0.261,1.862 1.84,0 0,1 -0.298,-0.738 1.994,1.972 0,0 1,-0.026 -0.39,2.092 2.069,0 0,1 0.053,-0.39 2.16,2.136 0,0 1,0.124 -0.374,2.208 2.183,0 0,1 0.194,-0.35 2.152,2.128 0,0 1,0.258 -0.309,2.087 2.063,0 0,1 0.319,-0.257 2.166,2.141 0,0 1,0.425 -0.22,2.227 2.202,0 0,1 0.451,-0.12 2.358,2.332 0,0 1,0.462 -0.027,2.45 2.422,0 0,1 0.46,0.063 2.52,2.491 0,0 1,1.214 0.673,2.425 2.398,0 0,1 0.301,0.371 2.523,2.495 0,0 1,0.249 0.486,2.588 2.559,0 0,1 0.135,0.512 2.71,2.68 0,0 1,0.028 0.525,2.851 2.82,0 0,1 -0.248,1.018 2.851,2.82 0,0 1,-0.615 0.87,2.811 2.78,0 0,1 -0.43,0.338 2.895,2.862 0,0 1,-0.557 0.273,2.937 2.904,0 0,1 -0.587,0.148 3.109,3.074 0,0 1,-0.598 0.027,3.167 3.131,0 0,1 -0.592,-0.086 3.259,3.222 0,0 1,-1.092 -0.492,3.197 3.16,0 0,1 -0.464,-0.393 3.13,3.095 0,0 1,-0.383 -0.483,3.229 3.193,0 0,1 -0.462,-1.254 3.404,3.366 0,0 1,-0.03 -0.66,3.52 3.48,0 0,1 0.1,-0.652 3.563,3.523 0,0 1,0.223 -0.625,3.619 3.578,0 0,1 0.334,-0.575 3.53,3.49 0,0 1,0.446 -0.508,3.545 3.505,0 0,1 1.232,-0.75 3.623,3.582 0,0 1,0.723 -0.176,3.816 3.773,0 0,1 0.735,-0.03 3.911,3.868 0,0 1,0.727 0.11,3.973 3.928,0 0,1 0.695,0.244 3.921,3.877 0,0 1,1.206 0.854,3.786 3.743,0 0,1 0.466,0.597 3.897,3.853 0,0 1,0.362 0.744,3.99 3.945,0 0,1 0.192,0.782 4.17,4.123 0,0 1,0.03 0.796,4.293 4.245,0 0,1 -0.123,0.785 4.375,4.326 0,0 1,-0.27 0.752,4.319 4.27,0 0,1 -0.41,0.691 4.275,4.228 0,0 1,-0.54 0.609,4.096 4.05,0 0,1 -0.316,0.265 3.93,3.885 0,0 1,-0.346 0.236,4.154 4.108,0 0,1 -0.402,0.22 4.24,4.193 0,0 1,-0.415 0.17,4.321 4.273,0 0,1 -0.427,0.124 4.378,4.329 0,0 1,-0.433 0.08,4.332 4.284,0 0,1 -0.436,0.036 4.258,4.21 0,0 1,-0.438 -0.007,4.508 4.508,0 0,1 -0.862,-0.135 4.96,4.96 0,0 1,-0.824 -0.293,4.65 4.65,0 0,1 -1.104,-0.715 4.582,4.53 0,0 1,-0.61 -0.647,4.505 4.454,0 0,1 -0.257,-0.372 4.716,4.664 0,0 1,-0.235 -0.43,4.646 4.594,0 0,1 -0.185,-0.441 4.745,4.692 0,0 1,-0.135 -0.456,4.97 4.97,0 0,1 -0.086,-0.462 4.329,4.329 0,0 1,-0.037 -0.467c-0.005,-0.159 0,-0.312 0.007,-0.467a4.892,4.892 0,0 1,0.288 -1.366,4.25 4.25,0 0,1 0.182,-0.433c0.07,-0.14 0.142,-0.278 0.223,-0.414a4.85,4.85 0,0 1,0.561 -0.763c0.105,-0.116 0.223,-0.235 0.334,-0.341a4.774,4.72 0,0 1,0.373 -0.31,4.884 4.83,0 0,1 0.406,-0.274 5.001,4.945 0,0 1,0.47 -0.247,5.08 5.024,0 0,1 0.482,-0.195 4.951,4.896 0,0 1,0.496 -0.142,5.021 4.965,0 0,1 0.502,-0.09 5.04,5.04 0,0 1,1.014 -0.032,5.121 5.121,0 0,1 0.999,0.16 5.811,5.811 0,0 1,0.954 0.34,5.646 5.646,0 0,1 0.872,0.514 5.43,5.43 0,0 1,0.77 0.678,5.26 5.2,0 0,1 0.63,0.83c0.059,0.095 0.112,0.193 0.167,0.292a0.051,0.05 0,0 0,0.033 0.025,0.053 0.053,0 0,0 0.042,-0.007 0.051,0.05 0,0 0,0.02 -0.062,5.5 5.438,0 1,0 -7.524,7.003 5.441,5.38 0,0 0,2.462 0.58,5.51 5.449,0 0,0 1.704,-0.269c0.126,-0.046 0.272,-0.102 0.415,-0.17a5.122,5.064 0,0 0,0.49 -0.26z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_home_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_hulu.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"34dp\"\n    android:height=\"50dp\"\n    android:viewportWidth=\"34\"\n    android:viewportHeight=\"50\">\n  <path\n      android:pathData=\"M22.2222,13.8889L11.1111,13.8889L11.1111,0L0,0L0,25L0,50L11.1111,50L11.1111,27.7778C11.1111,26.3889 12.2222,25 13.8889,25L19.4444,25C20.8333,25 22.2222,26.1111 22.2222,27.7778L22.2222,50L33.3333,50L33.3333,25C33.3333,18.8889 28.3333,13.8889 22.2222,13.8889L22.2222,13.8889Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#8BC34A\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_imdb.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"512\"\n    android:viewportWidth=\"512\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#f5c518\" android:pathData=\"M76.8,0L435.2,0A76.8,76.8 0,0 1,512 76.8L512,435.2A76.8,76.8 0,0 1,435.2 512L76.8,512A76.8,76.8 0,0 1,0 435.2L0,76.8A76.8,76.8 0,0 1,76.8 0z\"/>\n    <path android:fillColor=\"#FF000000\" android:pathData=\"M104,328L104,184L64,184v144zM189,184l-9,67 -5,-36 -5,-31h-50v144h34v-95l14,95h25l13,-97v97h34L240,184zM256,328L256,184h62c15,0 26,11 26,25v94c0,14 -11,25 -26,25zM303,210l-9,-1v94c5,0 9,-1 10,-3 2,-2 2,-8 2,-18v-56,-12l-3,-4zM419,220h3c14,0 26,11 26,25v58c0,14 -12,25 -26,25h-3c-8,0 -16,-4 -21,-11l-2,9h-36L360,184h38v46c5,-6 13,-10 21,-10zM411,290v-34l-1,-11c-1,-2 -4,-3 -6,-3s-5,1 -6,3v57c1,2 4,3 6,3s6,-1 6,-3l1,-12z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_incomplete_circle_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12c0,-2.76 1.12,-5.26 2.93,-7.07L12,12V2C17.52,2 22,6.48 22,12z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_insert_photo_48.xml",
    "content": "<vector android:height=\"48dp\" android:tint=\"?attr/colorControlNormal\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"48dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_instagram.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorOnSurface\" android:fillType=\"evenOdd\" android:pathData=\"M12,18C15.314,18 18,15.314 18,12C18,8.686 15.314,6 12,6C8.686,6 6,8.686 6,12C6,15.314 8.686,18 12,18ZM12,16C14.209,16 16,14.209 16,12C16,9.791 14.209,8 12,8C9.791,8 8,9.791 8,12C8,14.209 9.791,16 12,16Z\"/>\n    <path android:fillColor=\"?attr/colorOnSurface\" android:pathData=\"M18,5C17.448,5 17,5.448 17,6C17,6.552 17.448,7 18,7C18.552,7 19,6.552 19,6C19,5.448 18.552,5 18,5Z\"/>\n    <path android:fillColor=\"?attr/colorOnSurface\" android:fillType=\"evenOdd\" android:pathData=\"M1.654,4.276C1,5.56 1,7.24 1,10.6V13.4C1,16.76 1,18.441 1.654,19.724C2.229,20.853 3.147,21.771 4.276,22.346C5.56,23 7.24,23 10.6,23H13.4C16.76,23 18.441,23 19.724,22.346C20.853,21.771 21.771,20.853 22.346,19.724C23,18.441 23,16.76 23,13.4V10.6C23,7.24 23,5.56 22.346,4.276C21.771,3.147 20.853,2.229 19.724,1.654C18.441,1 16.76,1 13.4,1H10.6C7.24,1 5.56,1 4.276,1.654C3.147,2.229 2.229,3.147 1.654,4.276ZM13.4,3H10.6C8.887,3 7.722,3.002 6.822,3.075C5.945,3.147 5.497,3.277 5.184,3.436C4.431,3.819 3.819,4.431 3.436,5.184C3.277,5.497 3.147,5.945 3.075,6.822C3.002,7.722 3,8.887 3,10.6V13.4C3,15.113 3.002,16.278 3.075,17.178C3.147,18.055 3.277,18.503 3.436,18.816C3.819,19.569 4.431,20.181 5.184,20.564C5.497,20.723 5.945,20.853 6.822,20.925C7.722,20.998 8.887,21 10.6,21H13.4C15.113,21 16.278,20.998 17.178,20.925C18.055,20.853 18.503,20.723 18.816,20.564C19.569,20.181 20.181,19.569 20.564,18.816C20.723,18.503 20.853,18.055 20.925,17.178C20.998,16.278 21,15.113 21,13.4V10.6C21,8.887 20.998,7.722 20.925,6.822C20.853,5.945 20.723,5.497 20.564,5.184C20.181,4.431 19.569,3.819 18.816,3.436C18.503,3.277 18.055,3.147 17.178,3.075C16.278,3.002 15.113,3 13.4,3Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_keyboard_arrow_down_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_kickstarter.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"1024\"\n    android:viewportWidth=\"1024\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#05ce78\" android:pathData=\"M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0\"/>\n    <path android:fillColor=\"#fff\" android:pathData=\"m656,512.1 l46.6,-46.2c48.2,-47.9 48.2,-125.9 0,-173.8s-126.8,-47.9 -175,0l-17,16.8c-22.4,-32 -59.4,-52.9 -101.7,-52.9 -68.3,0 -123.7,55 -123.7,122.8v266.3c0,67.8 55.4,122.8 123.7,122.8 42.3,0 79.3,-20.9 101.7,-52.9l17,16.8c48.2,47.9 126.8,47.9 175,0s48.2,-125.9 0,-173.8L656,512.1z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_lastfm.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"800dp\"\n    android:height=\"800dp\"\n    android:viewportWidth=\"512\"\n    android:viewportHeight=\"512\">\n  <path\n      android:pathData=\"M429.86,220.01c-4.37,-1.5 -8.6,-2.88 -12.66,-4.22c-32.22,-10.57 -43.37,-15.14 -43.37,-33.08c0,-15.56 11.05,-26.43 26.87,-26.43c12.98,0 21.95,5.38 30.95,18.55c3.5,5.13 10.32,6.81 15.8,3.88l31.12,-16.59c2.86,-1.52 5,-4.13 5.94,-7.24c0.94,-3.11 0.6,-6.46 -0.95,-9.31c-18.08,-33.48 -45.37,-50.45 -81.11,-50.45c-26.97,0 -49.81,8.5 -66.08,24.57c-16.12,15.93 -24.64,38.12 -24.64,64.16c0,54.63 34.62,77.22 94.38,97.78c34.21,11.88 42.2,16.48 42.2,34.33c0,22.07 -19.06,38.09 -45.32,38.09c-0.79,0 -1.59,-0.01 -2.41,-0.04c-28.25,-0.99 -36.94,-14.77 -50.04,-45.99c-21.04,-50.08 -45.64,-110.1 -47.47,-114.66c-0.02,-0.05 -0.04,-0.1 -0.06,-0.16c-26.68,-64.26 -79.74,-101.11 -145.56,-101.11C70.64,92.08 0,165.63 0,256.03c0,90.37 70.64,163.89 157.46,163.89c47.49,0 91.93,-21.93 121.93,-60.16c2.72,-3.47 3.36,-8.13 1.67,-12.2l-18.76,-45.15c-1.83,-4.41 -6.06,-7.34 -10.82,-7.51c-4.79,-0.17 -9.19,2.46 -11.33,6.72c-16.22,32.35 -47.9,52.44 -82.68,52.44c-51.46,0 -93.32,-43.97 -93.32,-98.02c0,-54.06 41.86,-98.04 93.32,-98.04c37.43,0 71.68,23.2 85.22,57.73c0.04,0.09 0.07,0.18 0.11,0.27l46.44,110.5l5.35,12.38c22.45,54.62 55.71,79.08 107.86,79.31h0.22c62.33,0 109.34,-43.4 109.34,-100.94C512,259.53 480.57,237.25 429.86,220.01z\"\n      android:fillColor=\"#B4002B\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<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:pathData=\"M54,80C67.2,80 78,70.326 78,55.814C78,46.237 71.833,35.942 61.2,32.681V34.047L54,31.628C53.342,33.618 53.406,40.706 53.598,46.697C51.297,45.793 42,46.284 42,48.143C42,48.748 42,49.353 44.4,49.353C45.127,49.353 45.963,49.297 46.877,49.236C48.894,49.103 51.285,48.944 53.691,49.304C53.751,50.831 53.814,52.204 53.867,53.29C52.164,52.188 42,52.258 42,54.605C42,55.209 42,55.814 44.4,55.814C45.127,55.814 45.963,55.758 46.877,55.698C48.98,55.559 51.49,55.392 54,55.814C57,55.814 62.076,59.5 61,59.5C60.481,59.5 59.779,59.275 58.871,58.983C56.628,58.264 53.124,57.139 48,58C37.8,60.419 33.6,60.651 32.4,44.93C30,49.767 30,51.581 30,55.814C30,69.116 40.8,80 54,80Z\"\n      android:fillColor=\"#E14B2A\"/>\n  <path\n      android:pathData=\"M30.12,58.233C30.041,57.437 30,56.63 30,55.814C30,51.581 30,49.767 32.4,44.93C32.903,51.523 33.934,55.31 35.713,57.306C35.379,58.131 34.723,58.837 33.6,58.837C32.765,58.837 32.446,58.707 32.105,58.567C31.723,58.411 31.313,58.244 30.12,58.233Z\"\n      android:fillColor=\"#FFE8D2\"/>\n  <path\n      android:pathData=\"M66,28C67.2,30.419 68.4,41.302 66,43.721C64.359,41.907 60.881,37.674 60.093,35.256C59.306,32.837 63.703,29.411 66,28Z\"\n      android:fillColor=\"#FF603D\"/>\n  <path\n      android:pathData=\"M65.099,30.419C65.879,32.093 66.659,39.628 65.099,41.302C64.033,40.047 61.772,37.116 61.261,35.442C60.749,33.767 63.606,31.395 65.099,30.419Z\"\n      android:fillColor=\"#FFE8D2\"/>\n  <path\n      android:pathData=\"M42.006,28C40.806,30.419 39.606,41.302 42.006,43.721C43.646,41.907 47.125,37.674 47.912,35.256C48.7,32.837 44.303,29.411 42.006,28Z\"\n      android:fillColor=\"#FF603D\"/>\n  <path\n      android:pathData=\"M42.906,30.419C42.126,32.093 41.346,39.628 42.906,41.302C43.972,40.047 46.233,37.116 46.745,35.442C47.257,33.768 44.399,31.395 42.906,30.419Z\"\n      android:fillColor=\"#FFE8D2\"/>\n  <path\n      android:pathData=\"M66,43.116C66,43.319 65.994,43.521 65.984,43.721L66,43.721L54,55.814L42,43.721L42.016,43.721C42.006,43.521 42,43.319 42,43.116C42,36.771 47.373,31.628 54,31.628C60.627,31.628 66,36.771 66,43.116Z\"\n      android:fillColor=\"#FF603D\"/>\n  <path\n      android:pathData=\"M46.861,41.545C47.229,41.452 47.849,41.395 48.487,41.532C49.121,41.668 49.756,41.992 50.192,42.65C50.268,42.765 50.423,42.796 50.538,42.72C50.653,42.644 50.685,42.489 50.609,42.374C50.084,41.58 49.319,41.199 48.592,41.043C47.87,40.888 47.171,40.951 46.739,41.06C46.605,41.094 46.524,41.229 46.558,41.363C46.591,41.497 46.727,41.578 46.861,41.545Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M61.139,41.545C60.771,41.452 60.151,41.395 59.513,41.532C58.879,41.668 58.244,41.992 57.808,42.65C57.732,42.765 57.577,42.796 57.462,42.72C57.347,42.644 57.315,42.489 57.391,42.374C57.916,41.58 58.681,41.199 59.407,41.043C60.13,40.888 60.829,40.951 61.261,41.06C61.395,41.094 61.476,41.229 61.442,41.363C61.409,41.497 61.273,41.578 61.139,41.545Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M40.8,43.721C42,43.721 44.7,46.744 47.4,49.767C50.1,52.791 52.8,55.814 54,55.814C55.2,55.814 57.9,52.791 60.6,49.767C63.3,46.744 66,43.721 67.2,43.721C61.844,43.721 58.401,43.721 55.162,52.322C54.448,52.141 53.552,52.141 52.838,52.322C49.599,43.721 46.156,43.721 40.8,43.721Z\"\n      android:fillColor=\"#FFE8D2\"/>\n  <path\n      android:pathData=\"M52.8,52.186C52.8,53.242 53.556,54.605 54,54.605C54.444,54.605 55.2,53.242 55.2,52.186C55.2,50.977 52.8,50.977 52.8,52.186Z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_letterboxd.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"500\"\n    android:viewportWidth=\"500\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#202830\" android:fillType=\"evenOdd\"\n        android:pathData=\"M250,250m-250,0a250,250 0,1 1,500 0a250,250 0,1 1,-500 0\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    <path android:fillColor=\"#00E054\" android:fillType=\"evenOdd\"\n        android:pathData=\"M179.92,249.97a70.08,69.97 0,1 0,140.16 0a70.08,69.97 0,1 0,-140.16 0z\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    <group>\n        <clip-path android:pathData=\"M309.15,180h129.85v141.39h-129.85z\"/>\n        <path android:fillColor=\"#40BCF4\" android:fillType=\"evenOdd\"\n            android:pathData=\"M298.84,249.97a70.08,69.97 0,1 0,140.16 0a70.08,69.97 0,1 0,-140.16 0z\"\n            android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    </group>\n    <group>\n        <clip-path android:pathData=\"M61,180h129.85v141.39h-129.85z\"/>\n        <path android:fillColor=\"#FF8000\" android:fillType=\"evenOdd\"\n            android:pathData=\"M61,249.97a70.08,69.97 0,1 0,140.16 0a70.08,69.97 0,1 0,-140.16 0z\"\n            android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    </group>\n    <path android:fillColor=\"#FFFFFF\" android:fillType=\"evenOdd\"\n        android:pathData=\"M190.54,287.02C183.81,276.28 179.92,263.58 179.92,249.97C179.92,236.37 183.81,223.67 190.54,212.92C197.27,223.67 201.16,236.37 201.16,249.97C201.16,263.58 197.27,276.28 190.54,287.02Z\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    <path android:fillColor=\"#FFFFFF\" android:fillType=\"evenOdd\"\n        android:pathData=\"M309.46,212.92C316.19,223.67 320.08,236.37 320.08,249.97C320.08,263.58 316.19,276.28 309.46,287.02C302.73,276.28 298.84,263.58 298.84,249.97C298.84,236.37 302.73,223.67 309.46,212.92Z\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_location_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_medium.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorOnSurface\" android:pathData=\"M13,12C13,15.314 10.314,18 7,18C3.686,18 1,15.314 1,12C1,8.686 3.686,6 7,6C10.314,6 13,8.686 13,12Z\"/>\n    <path android:fillColor=\"?attr/colorOnSurface\" android:pathData=\"M23,12C23,14.761 22.552,17 22,17C21.448,17 21,14.761 21,12C21,9.239 21.448,7 22,7C22.552,7 23,9.239 23,12Z\"/>\n    <path android:fillColor=\"?attr/colorOnSurface\" android:pathData=\"M17,18C18.657,18 20,15.314 20,12C20,8.686 18.657,6 17,6C15.343,6 14,8.686 14,12C14,15.314 15.343,18 17,18Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_mobcrush.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"250\"\n    android:viewportWidth=\"250\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#F9E900\" android:fillType=\"evenOdd\"\n        android:pathData=\"M32,0L218,0A32,32 0,0 1,250 32L250,218A32,32 0,0 1,218 250L32,250A32,32 0,0 1,0 218L0,32A32,32 0,0 1,32 0z\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    <path android:fillAlpha=\"0.25\" android:fillColor=\"#000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M250,208L250,218C250,235.67 235.67,250 218,250L32,250C14.33,250 0,235.67 0,218L0,208C0,225.67 14.33,240 32,240L218,240C235.67,240 250,225.67 250,208Z\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    <path android:fillAlpha=\"0.09879557\" android:fillColor=\"#000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M250.13,180.22C249.91,202.51 249.44,215.12 248.71,218.05C248.22,220 246.87,222.52 244.65,225.61C239.19,233.87 229.99,239.46 219.46,239.95C217.01,239.97 215.2,239.99 214.05,239.99L95.4,239.99L60.15,176.72L75.03,110.34L90.95,72.25L103.42,91.7L127.02,91.7L142.49,123.58L190.88,73.55L250.13,180.22Z\"\n        android:strokeAlpha=\"0.09879557\" android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    <path android:fillColor=\"#010101\" android:fillType=\"evenOdd\"\n        android:pathData=\"M159.36,71.69l0,19.01l-16.33,0l0,32.59l-16.33,0l0,-32.59l-35.38,0l0,-19.01l-32.66,0l0,103.21l32.66,0l0,-51.6l16.33,0l0,32.59l35.38,0l0,-32.59l16.33,0l0,51.6l32.66,0l0,-103.21l-32.66,0\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_more_vert.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"?attr/colorControlNormal\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_netflix.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"26dp\"\n    android:height=\"50dp\"\n    android:viewportWidth=\"26\"\n    android:viewportHeight=\"50\">\n  <path\n      android:pathData=\"M0.0567,0.2577C2.518,0.2526 4.982,0.2629 7.4459,0.2526C10.3041,8.0129 13.067,15.8093 15.9021,23.5773C16.4253,25.018 16.9046,26.4742 17.4923,27.8892C17.5696,18.6804 17.5026,9.4691 17.5258,0.2577L25.2887,0.2577L25.2887,46.6186C22.4768,46.9897 19.6521,47.2448 16.8325,47.5747C15.6289,44.1237 14.4356,40.6727 13.2191,37.2268C11.4227,32.0825 9.6624,26.9253 7.817,21.799C7.9459,30.7526 7.8325,39.7113 7.8737,48.6701C5.2706,49.0593 2.6469,49.3067 0.0593,49.7887C0.0515,33.2784 0.0593,16.768 0.0567,0.2577L0.0567,0.2577Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#E21221\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_notification_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<inset xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:drawable=\"@drawable/ic_launcher_foreground\"\n    android:inset=\"-24dp\"/>"
  },
  {
    "path": "app/src/main/res/drawable/ic_open_in_browser_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_open_in_new_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_osu.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"300\"\n    android:viewportWidth=\"300\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#f6a\" android:pathData=\"M150,150m-143.7,0a143.7,143.7 0,1 1,287.4 0a143.7,143.7 0,1 1,-287.4 0\"/>\n    <group>\n        <clip-path android:pathData=\"M150,150m-150,0a150,150 0,1 1,300 0a150,150 0,1 1,-300 0\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#f1f1f2\"\n            android:pathData=\"M-106.2,311.9 L150,-131.9l256.2,443.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#231f20\"\n            android:pathData=\"M43.8,418.7 L300,-25l256.2,443.7z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"m-434.8,338.4 l256.2,-443.7 256.2,443.7z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#929497\"\n            android:pathData=\"m36.4,133.2 l256.2,-443.8 256.2,443.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#636466\"\n            android:pathData=\"M-247.4,355 L8.8,-88.7 265,355z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m-125.1,621.9 l256.2,-443.8 256.2,443.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a7a8ab\"\n            android:pathData=\"m-189.9,122.2 l97.3,-168.5 97.3,168.5zM-108.4,498.8 L-11.1,330.3 86.2,498.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#6d6e70\"\n            android:pathData=\"m109.3,195.8 l97.3,-168.5 97.3,168.5z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a7a8ab\"\n            android:pathData=\"M273.7,460.7 L371,292.2l97.2,168.5z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m83.4,596.4 l48.6,-84.2 48.7,84.2z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#221f1f\"\n            android:pathData=\"m253.7,280.9 l48.6,-84.2 48.6,84.2zM21.2,375l97.3,-168.5L215.8,375z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"M83.8,-111.6 L11.5,13.6h194.6L133.8,-111.6zM297,-111.6 L273.4,-70.7h97.3L347,-111.6z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#404041\"\n            android:pathData=\"M10.3,307.5 L107.6,139l97.3,168.5z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"m211.1,344.2 l97.2,-168.5 97.3,168.5z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m-76.9,403.8 l48.1,-83.3 48.1,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#58595b\"\n            android:pathData=\"m98.2,79.8 l48.1,-83.3 48.1,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#221f1f\"\n            android:pathData=\"m258,65.2 l48.1,-83.3 48.1,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"m-86.9,306.6 l24.1,-41.7 24,41.7z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m107.5,596.6 l24,-41.7 24.1,41.7zM191.7,440.6 L215.7,398.9 239.7,440.6z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#404041\"\n            android:pathData=\"m172.1,234.7 l48.1,-83.3 48.1,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#f1f1f2\"\n            android:pathData=\"m258,214.3 l48.1,-83.3 48.1,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a7a8ab\"\n            android:pathData=\"M-74.5,53.8 L-26.4,-29.5l48.1,83.3zM-122.9,200.4 L-74.8,117.1 -26.7,200.4z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"m263.2,-45 l24,-41.6L311.3,-45z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#bbbdbf\"\n            android:pathData=\"m162.2,91.2 l24.1,-41.6 24,41.6z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#fff\"\n            android:pathData=\"m206.9,38.8 l6.4,-11 6.4,11z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#6d6e70\"\n            android:pathData=\"M3.9,92.8 L52,9.5l48.1,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a6a8ab\"\n            android:pathData=\"m85.8,306.9 l48.1,-83.3 48.1,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#231f20\"\n            android:pathData=\"m82,238.1 l18.1,-31.4 18,31.4z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a7a8ab\"\n            android:pathData=\"m189,260.3 l24,-41.7 24.1,41.7zM183.6,142.2 L207.6,100.6 231.7,142.2z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#f1f1f2\"\n            android:pathData=\"m201,77.8 l48.1,-83.3 48,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a6a8ab\"\n            android:pathData=\"m-23.8,118.6 l48.1,-83.3 48.1,83.3z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m-11,247.2 l18.3,-31.8 18.3,31.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a6a8ab\"\n            android:pathData=\"m240.4,205.6 l18.3,-31.8 18.3,31.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#fff\"\n            android:pathData=\"M131.7,59.8 L150,28.1l18.3,31.7z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#58595b\"\n            android:pathData=\"m131.7,257.1 l18.3,-31.8 18.3,31.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m-44.7,63 l18.3,-31.8L-8,63zM213.4,335.2 L231.7,303.5 250,335.2z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"m282.5,110.6 l18.3,-31.8 18.3,31.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#bbbdbf\"\n            android:pathData=\"m20.3,208.1 l18.3,-31.8L57,208.1z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#f1f1f2\"\n            android:pathData=\"m47.3,270.1 l18.3,-31.8 18.4,31.8zM2.6,142.7 L21,111l18.3,31.7z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m88.7,98.7 l9.1,-15.8 9.2,15.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#bbbdbf\"\n            android:pathData=\"m25.8,127.9 l9.1,-15.8 9.2,15.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#e6e7e8\"\n            android:pathData=\"m82.7,59.4 l9.2,-15.9 9.2,15.9z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#221f1f\"\n            android:pathData=\"m44,335.2 l18.3,-31.7 18.3,31.7z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m87.2,394.2 l18.3,-31.8 18.3,31.8zM491.7,263.1 L510.1,231.3 528.4,263.1z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"M162.7,36.9 L181,5.1l18.4,31.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#fff\"\n            android:pathData=\"m258.4,103.2 l18.3,-31.7 18.3,31.7z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m291.6,597.1 l9.2,-15.9 9.2,15.9z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a7a8ab\"\n            android:pathData=\"m237.4,386.2 l9.2,-15.8 9.1,15.8z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"m552.5,369.4 l9.2,-15.9 9.2,15.9z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"m-122.4,314.9 l18.3,-31.7 18.4,31.7zM252.7,290.5 L271.1,258.7 289.4,290.5z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#a7a8ab\"\n            android:pathData=\"m261.9,243.6 l9.2,-15.9 9.1,15.9z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#d0d2d3\"\n            android:pathData=\"m185.1,103.5 l10.9,-18.9 10.9,18.9z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#808184\"\n            android:pathData=\"M69.9,-66.1 L79,-82l9.2,15.9z\" android:strokeAlpha=\"0.15\"/>\n        <path android:fillAlpha=\"0.15\" android:fillColor=\"#59595c\"\n            android:pathData=\"M361.7,96.9 L380,65.2l18.4,31.7zM438,311.8l9.2,-15.9 9.2,15.9z\" android:strokeAlpha=\"0.15\"/>\n    </group>\n    <path android:fillColor=\"#fff\" android:pathData=\"M75.1,181.4c-4.7,0 -8.8,-0.8 -12.3,-2.3 -3.5,-1.5 -6.4,-3.7 -8.6,-6.4 -2.3,-2.7 -4,-5.9 -5.2,-9.6 -1.2,-3.7 -1.7,-7.6 -1.7,-11.9 0,-4.3 0.6,-8.3 1.7,-12 1.2,-3.7 2.9,-7 5.2,-9.7 2.3,-2.7 5.2,-4.9 8.6,-6.5 3.5,-1.6 7.6,-2.4 12.3,-2.4s8.8,0.8 12.3,2.4c3.5,1.6 6.4,3.7 8.8,6.5 2.3,2.7 4,6 5.2,9.7 1.1,3.7 1.7,7.7 1.7,12s-0.6,8.2 -1.7,11.9c-1.1,3.7 -2.8,6.9 -5.2,9.6 -2.3,2.7 -5.2,4.9 -8.8,6.4 -3.4,1.6 -7.6,2.3 -12.3,2.3zM75.1,169.3c4.2,0 7.2,-1.6 9,-4.7 1.8,-3.1 2.7,-7.6 2.7,-13.4 0,-5.8 -0.9,-10.3 -2.7,-13.4 -1.8,-3.1 -4.8,-4.7 -9,-4.7 -4.1,0 -7.1,1.6 -8.9,4.7 -1.8,3.1 -2.8,7.6 -2.8,13.4 0,5.8 0.9,10.3 2.8,13.4 1.8,3.2 4.8,4.7 8.9,4.7zM126.9,154.8c-4.2,-1.2 -7.5,-3 -9.8,-5.3 -2.4,-2.4 -3.5,-5.9 -3.5,-10.6 0,-5.7 2,-10.1 6.1,-13.4 4.1,-3.2 9.6,-4.8 16.7,-4.8a49.13,49.13 0,0 1,17.2 3.2c-0.2,1.9 -0.5,4 -1.1,6.1 -0.6,2.1 -1.3,3.9 -2.1,5.5 -1.8,-0.7 -3.8,-1.4 -5.9,-2 -2.2,-0.6 -4.5,-0.8 -6.8,-0.8 -2.5,0 -4.5,0.4 -5.9,1.2 -1.4,0.8 -2.1,2 -2.1,3.8 0,1.6 0.5,2.8 1.5,3.5 1,0.7 2.4,1.3 4.3,1.9l6.4,1.9c2.1,0.6 4,1.3 5.7,2.2 1.7,0.9 3.1,1.9 4.3,3.2 1.2,1.3 2.1,2.8 2.8,4.7 0.7,1.9 1,4.2 1,6.8 0,2.8 -0.6,5.3 -1.7,7.7a17.6,17.6 0,0 1,-5 6.2c-2.2,1.8 -4.9,3.1 -8,4.2a35,35 0,0 1,-10.7 1.5c-1.8,0 -3.4,-0.1 -4.9,-0.2 -1.5,-0.1 -2.9,-0.3 -4.3,-0.6s-2.7,-0.6 -4.1,-1c-1.3,-0.4 -2.8,-0.9 -4.4,-1.5 0.1,-2 0.5,-4.1 1.1,-6.1 0.6,-2.1 1.3,-4.1 2.2,-6 2.5,1 4.8,1.7 7,2.2 2.2,0.5 4.5,0.7 6.9,0.7 1,0 2.2,-0.1 3.4,-0.3 1.2,-0.2 2.4,-0.5 3.4,-1s1.9,-1.1 2.6,-1.9c0.7,-0.8 1.1,-1.8 1.1,-3.1 0,-1.8 -0.5,-3.1 -1.6,-3.9 -1.1,-0.8 -2.6,-1.5 -4.5,-2.1l-7.3,-1.9zM166.2,122.1c2.7,-0.4 5.3,-0.7 8,-0.7 2.6,0 5.3,0.2 8,0.7v30.7c0,3.1 0.2,5.6 0.7,7.6 0.5,2 1.2,3.6 2.2,4.7 1,1.2 2.3,2 3.8,2.5s3.3,0.7 5.3,0.7c2.8,0 5.1,-0.3 7,-0.8v-45.4c2.7,-0.4 5.3,-0.7 7.9,-0.7 2.6,0 5.3,0.2 8,0.7v55.8c-2.4,0.8 -5.6,1.6 -9.5,2.4a65.33,65.33 0,0 1,-23.3 0.3c-3.5,-0.6 -6.6,-1.9 -9.3,-3.8 -2.7,-1.9 -4.8,-4.8 -6.3,-8.5 -1.6,-3.7 -2.4,-8.7 -2.4,-14.9v-31.3zM232.1,180.1c-0.4,-2.8 -0.7,-5.5 -0.7,-8.2 0,-2.7 0.2,-5.5 0.7,-8.3 2.8,-0.4 5.5,-0.7 8.2,-0.7 2.7,0 5.5,0.2 8.3,0.7 0.4,2.8 0.7,5.6 0.7,8.2 0,2.8 -0.2,5.5 -0.7,8.3 -2.8,0.4 -5.6,0.7 -8.2,0.7 -2.8,-0.1 -5.5,-0.3 -8.3,-0.7zM231.7,99.4c2.9,-0.4 5.8,-0.7 8.6,-0.7 2.9,0 5.8,0.2 8.8,0.7l-1.1,54.9c-2.6,0.4 -5.1,0.7 -7.5,0.7 -2.5,0 -5.1,-0.2 -7.6,-0.7l-1.2,-54.9z\"/>\n    <path android:fillColor=\"#fff\" android:pathData=\"M150,0C67.2,0 0,67.2 0,150s67.2,150 150,150 150,-67.2 150,-150S232.8,0 150,0zM150,285c-74.6,0 -135,-60.4 -135,-135S75.4,15 150,15s135,60.4 135,135 -60.4,135 -135,135z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_bookmarks_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"960\" android:viewportWidth=\"960\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M160,880L160,320Q160,287 183.5,263.5Q207,240 240,240L560,240Q593,240 616.5,263.5Q640,287 640,320L640,880L400,760L160,880ZM240,759L400,673L560,759L560,320Q560,320 560,320Q560,320 560,320L240,320Q240,320 240,320Q240,320 240,320L240,759ZM720,720L720,160Q720,160 720,160Q720,160 720,160L280,160L280,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,720L720,720ZM240,320L240,320Q240,320 240,320Q240,320 240,320L560,320Q560,320 560,320Q560,320 560,320L560,320L400,320L240,320Z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_explore_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"960\" android:viewportWidth=\"960\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M300,660L580,580L660,300L380,380L300,660ZM480,540Q455,540 437.5,522.5Q420,505 420,480Q420,455 437.5,437.5Q455,420 480,420Q505,420 522.5,437.5Q540,455 540,480Q540,505 522.5,522.5Q505,540 480,540ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q613,800 706.5,706.5Q800,613 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,613 253.5,706.5Q347,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_home_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,5.69l5,4.5V18h-2v-6H9v6H7v-7.81l5,-4.5M12,3L2,12h3v8h6v-6h2v6h6v-8h3L12,3z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_info_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_person_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2m0,10c2.7,0 5.8,1.29 6,2L6,18c0.23,-0.72 3.31,-2 6,-2m0,-12C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_view_list_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M3,5v14h18V5H3zM7,7v2H5V7H7zM5,13v-2h2v2H5zM5,15h2v2H5V15zM19,17H9v-2h10V17zM19,13H9v-2h10V13zM19,9H9V7h10V9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_patreon.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"20\"\n    android:viewportWidth=\"20\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorOnSurface\" android:fillType=\"evenOdd\"\n        android:pathData=\"M0,9.709C0,1.629 10.319,-3.476 16.916,2.818C20.448,6.178 21.059,11.72 18.164,15.769C14.573,20.688 9.882,19.962 5.403,19.962L5.403,10.374C5.445,8.256 6.151,6.431 8.728,5.558C10.973,4.894 13.591,6.139 14.381,8.505C15.213,11.038 14.007,12.739 12.594,13.777C11.181,14.816 8.978,14.816 7.523,13.819L7.523,17.098C10.662,18.598 15.629,16.794 17.208,12.698C18.289,9.834 17.54,6.513 15.296,4.438C12.595,2.237 9.643,1.698 6.485,3.234C4.281,4.355 2.744,6.638 2.369,9.128L2.369,19.962L0.042,19.962L0,9.709Z\"\n        android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_person_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_player_me.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"32\"\n    android:viewportWidth=\"32\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorOnSurface\" android:pathData=\"M15.974,0c-6.594,0 -11.943,5.349 -11.943,11.943v0.484c0.344,8.677 9.411,19.573 9.411,19.573v-14.396c-2.182,-0.995 -3.677,-3.156 -3.677,-5.667 0.005,-5.063 5.729,-8 9.839,-5.047 4.109,2.948 3.161,9.313 -1.635,10.932l-0.042,0.016v5.901c5.729,-0.948 10.042,-5.87 10.042,-11.792 0,-6.599 -5.349,-11.948 -11.948,-11.948z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_raptr.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"512\"\n    android:viewportWidth=\"512\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorOnSurface\" android:fillType=\"evenOdd\" android:pathData=\"M25.7,0h460.9C500.51,0 512,11.49 512,25.71v460.89c0,13.91 -11.49,25.4 -25.4,25.4H25.7C11.49,512 0,500.51 0,486.6V25.71C0,11.49 11.49,0 25.7,0L25.7,0zM295.77,415.53c6.06,-8.77 21.16,-22.37 17.55,-52.32c-0.62,-4.54 3.62,-4.84 4.84,-0.6C324.19,384.08 309.08,407.67 295.77,415.53L295.77,415.53zM267.64,133.37c2.13,0.6 2.42,16.33 -3.93,13.91C259.47,145.77 264.92,132.46 267.64,133.37L267.64,133.37zM160.28,206.56c3.33,8.77 7.26,20.86 33.87,33.27c101.31,44.76 201.43,5.75 189.93,-60.79c-7.57,-44.46 -67.15,-96.47 -117.36,-108.88c-75,-18.45 -130.04,42.94 -132.75,94.66c-2.12,39.01 7.55,69.86 9.37,108.87c0,1.51 -0.3,3.63 -1.21,3.94c-9.98,2.11 -26.92,6.34 -27.52,8.76c-1.21,4.54 1.82,23.29 3.03,29.34c0.3,2.42 1.81,2.72 2.72,0.6c1.82,-3.63 0,-12.39 2.12,-16.03c1.21,-2.42 5.14,-3.02 12.09,-2.72c3.64,0 12.4,-0.61 12.1,3.33c-0.3,1.51 -0.3,3.93 -2.12,5.74c-9.98,9.67 -15.73,28.13 -14.21,39.62c0.3,2.42 2.12,1.81 3.02,-0.91c3.63,-14.22 10.89,-23.29 18.45,-24.19c1.82,-0.31 1.82,2.42 2.12,3.63c1.52,9.37 5.45,21.16 8.77,29.33c7.25,19.36 17.84,37.5 33.27,51.41c2.12,2.12 0,2.73 -1.82,3.03c-8.46,1.82 -40.22,14.52 -45.36,36.29c0,1.81 4.54,-3.03 7.86,-4.24c2.42,-0.91 8.16,-0.61 22.37,-0.61l90.11,0.31c7.56,-0.61 13.91,-4.84 18.46,-7.56c35.07,-23.58 61.1,-49.29 85.88,-86.8c3.04,-4.84 35.69,-61.09 20.28,-45.06c-0.31,0.3 -40.53,36.89 -68.95,40.22c-10.89,1.21 -14.22,2.72 -19.97,-5.45c-29.95,-36.28 -82.56,-52.01 -84.98,-56.25c-3.33,-5.75 -4.54,-20.27 -8.47,-22.37c-9.37,-5.15 -19.96,-8.77 -33.27,-15.43C162.4,233.77 160.28,223.19 160.28,206.56L160.28,206.56zM158.46,318.15c9.37,-5.45 23.89,-16.94 25.4,-27.52c0,-2.72 4.54,-4.24 5.45,-0.3C192.03,304.84 173.9,314.82 158.46,318.15L158.46,318.15zM280.95,111.59c0.91,-7.56 -4.24,-6.66 -7.57,-2.12c-2.71,3.33 -9.37,11.8 -14.21,24.8c-3.94,10.89 -6.95,23.29 -6.95,30.55c0,4.54 0.3,6.05 1.51,6.35c2.42,0.6 4.54,-1.51 6.34,-3.93C270.96,153.02 279.13,131.25 280.95,111.59L280.95,111.59zM270.96,200.21c-37.49,0 -54.73,-18.75 -64.71,-30.24c-21.48,-25.71 -12.7,-65.02 16.63,-78.93c35.08,-16.33 87.71,-1.51 105.54,39.62C347.17,173.29 312.1,200.21 270.96,200.21L270.96,200.21z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_reddit.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"32\"\n    android:viewportWidth=\"32\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#FC471E\" android:pathData=\"M16,2C8.278,2 2,8.278 2,16C2,23.722 8.278,30 16,30C23.722,30 30,23.722 30,16C30,8.278 23.722,2 16,2Z\"/>\n    <path android:fillColor=\"#ffffff\" android:fillType=\"evenOdd\" android:pathData=\"M20.019,8.91C20.007,8.99 20,9.072 20,9.156C20,10.004 20.672,10.692 21.5,10.692C22.328,10.692 23,10.004 23,9.156C23,8.308 22.328,7.621 21.5,7.621C21.131,7.621 20.793,7.757 20.531,7.984L16.636,7L15.228,12.765C13.355,12.891 11.671,13.472 10.4,14.349C10.04,13.986 9.545,13.763 9,13.763C7.895,13.763 7,14.68 7,15.81C7,16.597 7.434,17.281 8.07,17.623C8.024,17.867 8,18.117 8,18.37C8,21.479 11.582,24 16,24C20.418,24 24,21.479 24,18.37C24,18.117 23.976,17.867 23.93,17.623C24.566,17.281 25,16.597 25,15.81C25,14.68 24.105,13.763 23,13.763C22.455,13.763 21.961,13.986 21.6,14.349C20.215,13.394 18.34,12.79 16.265,12.742L17.364,8.241L20.019,8.91ZM12.5,18.882C13.328,18.882 14,18.194 14,17.346C14,16.498 13.328,15.81 12.5,15.81C11.672,15.81 11,16.498 11,17.346C11,18.194 11.672,18.882 12.5,18.882ZM19.5,18.882C20.328,18.882 21,18.194 21,17.346C21,16.498 20.328,15.81 19.5,15.81C18.672,15.81 18,16.498 18,17.346C18,18.194 18.672,18.882 19.5,18.882ZM12.777,20.503C12.548,20.346 12.237,20.41 12.084,20.645C11.931,20.88 11.993,21.198 12.223,21.355C13.311,22.097 14.655,22.469 16,22.469C17.345,22.469 18.689,22.097 19.777,21.355C20.007,21.198 20.069,20.88 19.916,20.645C19.763,20.41 19.452,20.346 19.223,20.503C18.302,21.131 17.151,21.445 16,21.445C15.317,21.445 14.634,21.334 14,21.114C13.565,20.962 13.152,20.758 12.777,20.503Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_remove_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19,13H5v-2h14v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_restore_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_save_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_save_alt_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19,12v7L5,19v-7L3,12v7c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2zM13,12.67l2.59,-2.58L17,11.5l-5,5 -5,-5 1.41,-1.41L11,12.67L11,3h2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_schedule_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_search_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_settings_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_share_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_shortcut_library_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@color/ic_shortcut_fill\"\n      android:pathData=\"M4,14h2c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v2C3,13.55 3.45,14 4,14zM4,19h2c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v2C3,18.55 3.45,19 4,19zM4,9h2c0.55,0 1,-0.45 1,-1V6c0,-0.55 -0.45,-1 -1,-1H4C3.45,5 3,5.45 3,6v2C3,8.55 3.45,9 4,9zM9,14h11c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1H9c-0.55,0 -1,0.45 -1,1v2C8,13.55 8.45,14 9,14zM9,19h11c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1H9c-0.55,0 -1,0.45 -1,1v2C8,18.55 8.45,19 9,19zM8,6v2c0,0.55 0.45,1 1,1h11c0.55,0 1,-0.45 1,-1V6c0,-0.55 -0.45,-1 -1,-1H9C8.45,5 8,5.45 8,6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_shortcut_search_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@color/ic_shortcut_fill\"\n      android:pathData=\"M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_shortcut_settings_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@color/ic_shortcut_fill\"\n      android:pathData=\"M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_soundcloud.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"48\"\n    android:viewportWidth=\"48\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#FF8800\" android:pathData=\"M24,24m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0\"/>\n    <path android:fillColor=\"#ffffff\" android:fillType=\"evenOdd\" android:pathData=\"M13.16,26.865C13.21,26.865 13.252,26.824 13.259,26.768L13.527,24.66L13.259,22.504C13.251,22.447 13.21,22.408 13.16,22.408C13.109,22.408 13.067,22.448 13.061,22.505L12.824,24.66L13.061,26.767C13.067,26.824 13.109,26.865 13.16,26.865ZM12.273,26.064C12.321,26.064 12.361,26.025 12.369,25.97L12.577,24.66L12.369,23.326C12.361,23.271 12.322,23.233 12.273,23.233C12.223,23.233 12.183,23.272 12.176,23.327L12,24.66L12.176,25.97C12.183,26.026 12.223,26.064 12.273,26.064ZM14.223,22.103C14.215,22.035 14.165,21.986 14.103,21.986C14.039,21.986 13.988,22.035 13.982,22.103C13.982,22.104 13.758,24.66 13.758,24.66L13.982,27.124C13.988,27.193 14.039,27.242 14.103,27.242C14.165,27.242 14.215,27.193 14.223,27.124L14.478,24.66L14.223,22.103ZM15.053,27.34C15.127,27.34 15.187,27.282 15.194,27.203L15.434,24.662L15.194,22.033C15.187,21.955 15.127,21.895 15.053,21.895C14.979,21.895 14.919,21.955 14.913,22.034L14.701,24.662L14.913,27.203C14.919,27.282 14.979,27.34 15.053,27.34ZM16.011,27.382C16.097,27.382 16.167,27.314 16.173,27.224L16.173,27.224L16.4,24.662L16.173,22.223C16.167,22.134 16.097,22.065 16.011,22.065C15.925,22.065 15.857,22.134 15.851,22.225L15.651,24.662L15.851,27.224C15.856,27.314 15.925,27.382 16.011,27.382ZM17.371,24.663L17.16,20.697C17.154,20.596 17.074,20.517 16.979,20.517C16.881,20.517 16.801,20.596 16.797,20.697L16.61,24.663L16.797,27.225C16.802,27.325 16.882,27.404 16.979,27.404C17.075,27.404 17.154,27.326 17.16,27.224V27.226L17.371,24.663ZM17.951,27.408C18.059,27.408 18.149,27.32 18.154,27.208V27.21L18.352,24.663L18.154,19.79C18.149,19.678 18.059,19.59 17.951,19.59C17.843,19.59 17.754,19.678 17.749,19.79C17.749,19.791 17.573,24.663 17.573,24.663L17.749,27.209C17.754,27.32 17.843,27.408 17.951,27.408ZM18.933,19.163C18.811,19.163 18.714,19.261 18.71,19.385L18.546,24.663L18.71,27.184C18.714,27.306 18.811,27.404 18.933,27.404C19.052,27.404 19.15,27.306 19.155,27.182V27.184L19.338,24.663L19.155,19.385C19.15,19.261 19.052,19.163 18.933,19.163ZM19.922,27.408C20.054,27.408 20.161,27.303 20.165,27.167V27.168L20.335,24.664L20.165,19.207C20.161,19.071 20.054,18.965 19.922,18.965C19.789,18.965 19.683,19.071 19.679,19.207L19.527,24.664L19.679,27.168C19.682,27.303 19.789,27.408 19.922,27.408ZM20.919,27.406C21.062,27.406 21.179,27.291 21.182,27.143V27.146L21.339,24.663L21.182,19.345C21.179,19.199 21.062,19.083 20.919,19.083C20.774,19.083 20.658,19.199 20.655,19.346L20.516,24.663L20.655,27.144C20.658,27.291 20.774,27.406 20.919,27.406ZM22.35,24.664L22.207,19.541C22.204,19.382 22.079,19.258 21.923,19.258C21.767,19.258 21.642,19.383 21.638,19.541L21.512,24.664L21.638,27.13C21.642,27.286 21.766,27.411 21.923,27.411C22.078,27.411 22.204,27.286 22.207,27.128V27.13L22.35,24.664ZM22.936,27.416C23.1,27.416 23.238,27.28 23.24,27.112V27.114L23.368,24.665L23.24,18.568C23.238,18.401 23.1,18.264 22.936,18.264C22.77,18.264 22.634,18.401 22.632,18.568L22.516,24.663C22.516,24.668 22.632,27.114 22.632,27.114C22.634,27.28 22.77,27.416 22.936,27.416ZM23.944,17.692C23.766,17.692 23.621,17.837 23.619,18.016L23.486,24.666L23.619,27.079C23.621,27.256 23.767,27.4 23.944,27.4C24.12,27.4 24.266,27.255 24.268,27.077V27.079L24.413,24.666L24.268,18.015C24.266,17.837 24.12,17.692 23.944,17.692ZM24.866,27.418C24.874,27.418 32.995,27.423 33.048,27.423C34.678,27.423 36,26.101 36,24.471C36,22.84 34.679,21.518 33.048,21.518C32.644,21.518 32.258,21.601 31.907,21.747C31.672,19.087 29.441,17 26.72,17C26.054,17 25.405,17.131 24.832,17.353C24.608,17.439 24.55,17.528 24.548,17.7V27.069C24.55,27.25 24.689,27.4 24.866,27.418Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_splashscreen.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <shape android:shape=\"oval\">\n            <solid android:color=\"@color/ic_launcher_background\" />\n        </shape>\n    </item>\n    <item android:drawable=\"@drawable/ic_launcher_foreground\" />\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/ic_star_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_star_outline_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_steam.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"32\"\n    android:viewportWidth=\"32\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorOnSurface\" android:pathData=\"M18.102,12.129c0,-0 0,-0 0,-0.001 0,-1.564 1.268,-2.831 2.831,-2.831s2.831,1.268 2.831,2.831c0,1.564 -1.267,2.831 -2.831,2.831 -0,0 -0,0 -0.001,0h0c-0,0 -0,0 -0.001,0 -1.563,0 -2.83,-1.267 -2.83,-2.83 0,-0 0,-0 0,-0.001v0zM24.691,12.135c0,-2.081 -1.687,-3.768 -3.768,-3.768s-3.768,1.687 -3.768,3.768c0,2.081 1.687,3.768 3.768,3.768v0c2.08,-0.003 3.765,-1.688 3.768,-3.767v-0zM10.427,23.76l-1.841,-0.762c0.524,1.078 1.611,1.808 2.868,1.808 1.317,0 2.448,-0.801 2.93,-1.943l0.008,-0.021c0.155,-0.362 0.246,-0.784 0.246,-1.226 0,-1.757 -1.424,-3.181 -3.181,-3.181 -0.405,0 -0.792,0.076 -1.148,0.213l0.022,-0.007 1.903,0.787c0.852,0.364 1.439,1.196 1.439,2.164 0,1.296 -1.051,2.347 -2.347,2.347 -0.324,0 -0.632,-0.066 -0.913,-0.184l0.015,0.006zM15.974,1.004c-7.857,0.001 -14.301,6.046 -14.938,13.738l-0.004,0.054 8.038,3.322c0.668,-0.462 1.495,-0.737 2.387,-0.737 0.001,0 0.002,0 0.002,0h-0c0.079,0 0.156,0.005 0.235,0.008l3.575,-5.176v-0.074c0.003,-3.12 2.533,-5.648 5.653,-5.648 3.122,0 5.653,2.531 5.653,5.653s-2.531,5.653 -5.653,5.653h-0.131l-5.094,3.638c0,0.065 0.005,0.131 0.005,0.199 0,0.001 0,0.002 0,0.003 0,2.342 -1.899,4.241 -4.241,4.241 -2.047,0 -3.756,-1.451 -4.153,-3.38l-0.005,-0.027 -5.755,-2.383c1.841,6.345 7.601,10.905 14.425,10.905 8.281,0 14.994,-6.713 14.994,-14.994s-6.713,-14.994 -14.994,-14.994c-0,0 -0.001,0 -0.001,0h0z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_sync_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_trakt.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"144.8dp\"\n    android:height=\"144.8dp\"\n    android:viewportWidth=\"144.8\"\n    android:viewportHeight=\"144.8\">\n  <path\n      android:pathData=\"M29.5,111.8c10.6,11.6 25.9,18.8 42.9,18.8c8.7,0 16.9,-1.9 24.3,-5.3L56.3,85L29.5,111.8z\"\n      android:fillColor=\"#ED2224\"/>\n  <path\n      android:pathData=\"M56.1,60.6L25.5,91.1L21.4,87l32.2,-32.2h0l37.6,-37.6c-5.9,-2 -12.2,-3.1 -18.8,-3.1c-32.2,0 -58.3,26.1 -58.3,58.3c0,13.1 4.3,25.2 11.7,35l30.5,-30.5l2.1,2l43.7,43.7c0.9,-0.5 1.7,-1 2.5,-1.6L56.3,72.7L27,102l-4.1,-4.1l33.4,-33.4l2.1,2l51,50.9c0.8,-0.6 1.5,-1.3 2.2,-1.9l-55,-55L56.1,60.6z\"\n      android:fillColor=\"#ED2224\"/>\n  <path\n      android:pathData=\"M115.7,111.4c9.3,-10.3 15,-24 15,-39c0,-23.4 -13.8,-43.5 -33.6,-52.8L60.4,56.2L115.7,111.4zM74.5,66.8l-4.1,-4.1l28.9,-28.9l4.1,4.1L74.5,66.8zM101.9,27.1L68.6,60.4l-4.1,-4.1L97.8,23L101.9,27.1z\"\n      android:fillColor=\"#ED1C24\"/>\n  <path\n      android:pathData=\"M72.4,144.8C32.5,144.8 0,112.3 0,72.4C0,32.5 32.5,0 72.4,0s72.4,32.5 72.4,72.4C144.8,112.3 112.3,144.8 72.4,144.8zM72.4,7.3C36.5,7.3 7.3,36.5 7.3,72.4s29.2,65.1 65.1,65.1s65.1,-29.2 65.1,-65.1S108.3,7.3 72.4,7.3z\"\n      android:fillColor=\"#ED2224\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_tubitv.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"50dp\"\n    android:height=\"50dp\"\n    android:viewportWidth=\"50\"\n    android:viewportHeight=\"50\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M34.864,40.63a1.134,1.134 0,0 0,-1.44 -0.48c-6.007,2.626 -12.735,-1.775 -12.736,-8.33v-7.961c0,-0.626 0.508,-1.134 1.134,-1.134h11.362c0.627,0 1.134,-0.508 1.134,-1.134v-6.805c0,-0.626 -0.507,-1.134 -1.134,-1.134H21.822a1.134,1.134 0,0 1,-1.134 -1.134V1.134C20.688,0.508 20.18,0 19.554,0h-7.96v31.82c0.022,13.433 14.1,22.208 26.17,16.312 0.57,-0.274 0.805,-0.96 0.524,-1.526z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_tumblr.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"1024\"\n    android:viewportWidth=\"1024\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#36465d\" android:pathData=\"M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0\"/>\n    <path android:fillColor=\"#fff\" android:pathData=\"M566.7,768c-108.7,0 -150,-80.1 -150,-136.8V463.9h-51.5v-66.1c77.4,-28 96.3,-98 100.5,-138 0.3,-2.7 2.5,-3.8 3.7,-3.8h75v130.4h102.4v77.5H543.9v159.4c0.3,21.4 8,50.6 47.1,50.6h1.9c13.5,-0.4 31.7,-4.4 41.3,-8.9l24.7,73.1c-9.3,13.6 -51.2,29.3 -88.7,30h-3.8l0.3,-0.1z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_twitch.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"2800\"\n    android:viewportWidth=\"2400\" android:width=\"171.42857dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#FFFFFF\" android:pathData=\"M2200,1300l-400,400l-400,0l-350,350l0,-350l-450,0l0,-1500l1600,0z\"/>\n    <path android:fillColor=\"#9146FF\" android:pathData=\"M500,0L0,500v1800h600v500l500,-500h400l900,-900V0H500zM2200,1300l-400,400h-400l-350,350v-350H600V200h1600V1300z\"/>\n    <path android:fillColor=\"#9146FF\" android:pathData=\"M1700,550h200v600h-200z\"/>\n    <path android:fillColor=\"#9146FF\" android:pathData=\"M1150,550h200v600h-200z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_twitter.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"1200\"\n    android:viewportHeight=\"1227\">\n    <path\n        android:fillColor=\"?attr/colorOnSurface\"\n        android:pathData=\"M714.2,519.3L1160.9,0H1055L667.1,450.9L357.3,0H0L468.5,681.8L0,1226.4H105.9L515.5,750.2L842.7,1226.4H1200L714.1,519.3H714.2ZM569.2,687.8L521.7,619.9L144,79.7H306.6L611.4,515.7L658.9,583.6L1055.1,1150.3H892.5L569.2,687.9V687.8Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_view_list_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M3,14h4v-4H3V14zM3,19h4v-4H3V19zM3,9h4V5H3V9zM8,14h13v-4H8V14zM8,19h13v-4H8V19zM8,5v4h13V5H8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_vimeo.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"512\"\n    android:viewportWidth=\"512\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#1eb8eb\" android:pathData=\"M76.8,0L435.2,0A76.8,76.8 0,0 1,512 76.8L512,435.2A76.8,76.8 0,0 1,435.2 512L76.8,512A76.8,76.8 0,0 1,0 435.2L0,76.8A76.8,76.8 0,0 1,76.8 0z\"/>\n    <path android:fillColor=\"#ffffff\" android:pathData=\"m418,185c-19,109 -128,202 -161,223 -32,21 -62,-9 -73,-30 -12,-26 -49,-164 -59,-176 -9,-12 -39,12 -39,12l-13,-19s59,-71 104,-79c47,-10 47,73 59,118 11,45 18,70 27,70 10,0 29,-24 49,-63 21,-37 -1,-71 -41,-47 17,-95 166,-118 147,-9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_vrv.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportWidth=\"32\"\n    android:viewportHeight=\"32\">\n  <path\n      android:pathData=\"M26,21L16,27 6,21 6,12 26,12 26,21z\"\n      android:fillColor=\"#1b1a26\"/>\n  <path\n      android:pathData=\"m2.63,8.19l0,15.62l13.37,7.81l13.37,-7.81l0,-15.62l-13.37,-7.81l-13.37,7.81zM24.74,20.91l-8.74,5.09l-8.73,-5.1l0,-6.46l8.73,5.1l8.73,-5.1l0.01,6.47z\"\n      android:fillColor=\"#fd0\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_watch_later_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM16.2,16.2L11,13V7h1.5v5.2l4.5,2.7L16.2,16.2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_website.xml",
    "content": "<vector android:height=\"200dp\" android:viewportHeight=\"28.8\"\n    android:viewportWidth=\"28.8\" android:width=\"200dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"?attr/colorPrimary\"\n        android:pathData=\"M14.4,0L14.4,0A14.4,14.4 0,0 1,28.8 14.4L28.8,14.4A14.4,14.4 0,0 1,14.4 28.8L14.4,28.8A14.4,14.4 0,0 1,0 14.4L0,14.4A14.4,14.4 0,0 1,14.4 0z\" android:strokeWidth=\"0\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"M6.4,17.4L22.4,17.4\"\n        android:strokeColor=\"?attr/colorOnPrimary\" android:strokeLineCap=\"round\"\n        android:strokeLineJoin=\"round\" android:strokeWidth=\"2\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"M6.4,11.4L22.4,11.4\"\n        android:strokeColor=\"?attr/colorOnPrimary\" android:strokeLineCap=\"round\"\n        android:strokeLineJoin=\"round\" android:strokeWidth=\"2\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"M14.4,14.4m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0\"\n        android:strokeColor=\"?attr/colorOnPrimary\" android:strokeLineCap=\"round\"\n        android:strokeLineJoin=\"round\" android:strokeWidth=\"2\"/>\n    <path android:fillColor=\"?attr/colorOnPrimary\" android:pathData=\"M14.4,23.218L13.686,23.918C13.874,24.11 14.132,24.218 14.4,24.218C14.669,24.218 14.927,24.11 15.115,23.918L14.4,23.218ZM14.4,5.582L15.115,4.882C14.927,4.69 14.669,4.582 14.4,4.582C14.132,4.582 13.874,4.69 13.686,4.882L14.4,5.582ZM17,14.4C17,17.561 15.737,20.425 13.686,22.518L15.115,23.918C17.517,21.466 19,18.105 19,14.4L17,14.4ZM13.686,6.282C15.737,8.375 17,11.239 17,14.4L19,14.4C19,10.695 17.517,7.334 15.115,4.882L13.686,6.282ZM11.8,14.4C11.8,11.239 13.063,8.375 15.115,6.282L13.686,4.882C11.283,7.334 9.8,10.695 9.8,14.4L11.8,14.4ZM15.115,22.518C13.063,20.425 11.8,17.561 11.8,14.4L9.8,14.4C9.8,18.105 11.283,21.466 13.686,23.918L15.115,22.518Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_youtube.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M19.615,3.184c-3.604,-0.246 -11.631,-0.245 -15.23,0 -3.897,0.266 -4.356,2.62 -4.385,8.816 0.029,6.185 0.484,8.549 4.385,8.816 3.6,0.245 11.626,0.246 15.23,0 3.897,-0.266 4.356,-2.62 4.385,-8.816 -0.029,-6.185 -0.484,-8.549 -4.385,-8.816zM9,16v-8l8,3.993 -8,4.007z\"\n      android:fillColor=\"#ff0000\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/onboarding_login_logo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"320dp\"\n    android:height=\"160dp\"\n    android:viewportWidth=\"320\"\n    android:viewportHeight=\"160\">\n  <path\n      android:pathData=\"M0,80C0,124.18 35.82,160 80,160C112.81,160 141,140.25 153.34,112C153.92,110.68 154.46,109.35 154.96,108C159,106 161,106 165.04,108C165.54,109.35 166.08,110.68 166.66,112C179,140.25 207.2,160 240,160C284.18,160 320,124.18 320,80C320,35.82 284.18,0 240,0C207.2,0 179,19.75 166.66,48C166.08,49.32 165.54,50.65 165.04,52C161,54 159,54 154.96,52C154.46,50.65 153.92,49.32 153.34,48C141,19.75 112.81,0 80,0C35.82,0 0,35.82 0,80Z\"\n      android:fillColor=\"#312631\"/>\n  <path\n      android:pathData=\"M80.46,126C103.82,126 122.92,108.88 122.92,83.21C122.92,66.27 112.01,48.05 93.2,42.28V44.7L80.46,40.42C79.3,43.94 79.41,56.48 79.75,67.08C75.68,65.48 59.23,66.35 59.23,69.64C59.23,70.71 59.23,71.78 63.48,71.78C64.76,71.78 66.24,71.68 67.86,71.57C71.43,71.34 75.66,71.06 79.91,71.69C80.02,74.39 80.13,76.82 80.23,78.74C77.21,76.79 59.23,76.92 59.23,81.07C59.23,82.14 59.23,83.21 63.48,83.21C64.76,83.21 66.24,83.11 67.86,83C71.58,82.76 76.02,82.46 80.46,83.21C85.77,83.21 94.75,89.73 92.85,89.73C91.93,89.73 90.69,89.33 89.08,88.82C85.11,87.54 78.91,85.55 69.85,87.08C51.8,91.36 44.37,91.77 42.25,63.95C38,72.51 38,75.72 38,83.21C38,106.74 57.11,126 80.46,126Z\"\n      android:fillColor=\"#E14B2A\"/>\n  <path\n      android:pathData=\"M38.21,87.49C38.07,86.08 38,84.65 38,83.21C38,75.72 38,72.51 42.25,63.95C43.14,75.62 44.96,82.32 48.11,85.85C47.52,87.31 46.36,88.56 44.37,88.56C42.89,88.56 42.33,88.33 41.72,88.08C41.05,87.8 40.32,87.51 38.21,87.49Z\"\n      android:fillColor=\"#FFE8D2\"/>\n  <path\n      android:pathData=\"M101.69,34C103.82,38.28 105.94,57.53 101.69,61.81C98.79,58.6 92.64,51.12 91.24,46.84C89.85,42.56 97.63,36.5 101.69,34Z\"\n      android:fillColor=\"#FF603D\"/>\n  <path\n      android:pathData=\"M100.1,38.28C101.48,41.24 102.86,54.57 100.1,57.53C98.21,55.31 94.21,50.13 93.31,47.17C92.4,44.2 97.46,40.01 100.1,38.28Z\"\n      android:fillColor=\"#FFE8D2\"/>\n  <path\n      android:pathData=\"M59.24,34C57.12,38.28 54.99,57.53 59.24,61.81C62.14,58.6 68.3,51.12 69.69,46.84C71.08,42.56 63.3,36.5 59.24,34Z\"\n      android:fillColor=\"#FF603D\"/>\n  <path\n      android:pathData=\"M60.83,38.28C59.45,41.24 58.07,54.57 60.83,57.53C62.72,55.31 66.72,50.13 67.63,47.17C68.53,44.2 63.48,40.01 60.83,38.28Z\"\n      android:fillColor=\"#FFE8D2\"/>\n  <path\n      android:pathData=\"M101.69,60.74C101.69,61.1 101.68,61.46 101.66,61.81H101.69L80.46,83.21L59.23,61.81H59.26C59.24,61.46 59.23,61.1 59.23,60.74C59.23,49.52 68.74,40.42 80.46,40.42C92.19,40.42 101.69,49.52 101.69,60.74Z\"\n      android:fillColor=\"#FF603D\"/>\n  <path\n      android:pathData=\"M67.83,57.96C68.48,57.8 69.58,57.7 70.71,57.94C71.83,58.18 72.95,58.75 73.72,59.92C73.86,60.12 74.13,60.18 74.34,60.04C74.54,59.91 74.6,59.63 74.46,59.43C73.53,58.03 72.18,57.35 70.89,57.08C69.62,56.8 68.38,56.91 67.61,57.11C67.38,57.17 67.23,57.41 67.29,57.64C67.35,57.88 67.59,58.02 67.83,57.96Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M93.09,57.96C92.44,57.8 91.34,57.7 90.21,57.94C89.09,58.18 87.97,58.75 87.2,59.92C87.06,60.12 86.79,60.18 86.59,60.04C86.38,59.91 86.33,59.63 86.46,59.43C87.39,58.03 88.74,57.35 90.03,57.08C91.31,56.8 92.54,56.91 93.31,57.11C93.54,57.17 93.69,57.41 93.63,57.64C93.57,57.88 93.33,58.02 93.09,57.96Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M57.11,61.81C59.23,61.81 64.01,67.16 68.78,72.51C73.56,77.86 78.34,83.21 80.46,83.21C82.58,83.21 87.36,77.86 92.14,72.51C96.92,67.16 101.69,61.81 103.82,61.81C94.34,61.81 88.25,61.81 82.52,77.03C81.25,76.71 79.67,76.71 78.4,77.03C72.68,61.81 66.58,61.81 57.11,61.81Z\"\n      android:fillColor=\"#FFE8D2\"/>\n  <path\n      android:pathData=\"M78.34,76.79C78.34,78.66 79.68,81.07 80.46,81.07C81.25,81.07 82.58,78.66 82.58,76.79C82.58,74.65 78.34,74.65 78.34,76.79Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M220.73,19.44C219.46,19.66 218.69,20.37 217.48,22.36C216.9,23.33 216.41,24.22 216.37,24.34C216.35,24.44 216.15,24.96 215.91,25.49C215.26,26.96 214.64,29.01 214.34,30.79C214,32.78 213.9,36.19 214.12,37.78C214.22,38.5 214.2,38.54 213.17,39.13C211.46,40.07 209.28,41.54 208.05,42.53C207.41,43.04 206.8,43.52 206.68,43.6C206.56,43.7 206.02,43.54 205.37,43.2C203.42,42.15 200.21,41.18 197.54,40.82C196.31,40.66 192.38,40.66 191.39,40.82C188.11,41.34 187.46,41.57 186.7,42.47C186.09,43.16 185.85,43.82 185.85,44.79C185.85,45.59 186.41,47.13 186.9,47.63C187.04,47.79 187.4,48.23 187.66,48.62C187.93,49 188.21,49.36 188.27,49.42C188.33,49.48 188.87,50.11 189.48,50.83C192.4,54.28 196.45,57.94 199.93,60.28C203.26,62.5 204.49,63.08 209.93,65.14C211.15,65.6 212.32,66.04 212.54,66.14C212.97,66.35 216.29,67.62 216.47,67.66C216.54,67.68 216.76,67.76 216.98,67.86C217.2,67.94 217.84,68.2 218.39,68.4C218.95,68.6 219.58,68.84 219.8,68.95C220.02,69.05 220.75,69.33 221.41,69.57C222.07,69.81 222.8,70.09 223.02,70.19C224.76,70.94 227.43,71.89 228.36,72.09C229.29,72.29 229.61,72.27 230.27,72.01C231.44,71.56 232.17,70.78 232.55,69.57C232.59,69.41 232.65,63.55 232.65,56.58C232.65,45.35 232.59,42.15 232.25,40.66C232.21,40.44 232.13,40.03 232.07,39.75C232.01,39.47 231.79,38.48 231.57,37.54C231.34,36.59 231.12,35.68 231.08,35.52C231.02,35.26 230.64,34.15 229.63,31.39C229.23,30.28 226.35,24.48 225.3,22.63C223.77,19.97 222.54,19.12 220.73,19.44ZM221.61,25.45C224.05,30.28 225.18,32.62 225.52,33.55C225.9,34.63 225.9,34.72 225.58,34.74C225.4,34.74 225.1,34.78 224.94,34.82C224.77,34.86 224.41,34.92 224.13,34.98C223.85,35.04 223.22,35.18 222.72,35.32C222.22,35.46 221.73,35.58 221.61,35.62C221.51,35.64 221.27,35.7 221.11,35.76C219.34,36.37 218.65,36.57 218.57,36.47C218.43,36.33 218.43,33.77 218.57,32.8C218.65,32.36 218.75,31.67 218.81,31.29C219.07,29.5 220.83,25.04 221.27,25.04C221.35,25.04 221.51,25.23 221.61,25.45ZM197.03,45.47C198.97,45.72 202.23,46.68 202.81,47.19C202.9,47.25 202.45,47.95 201.85,48.72C201.22,49.5 200.62,50.27 200.5,50.43C200.36,50.59 199.85,51.34 199.35,52.08L198.44,53.43L197.56,52.59C195.08,50.19 194.31,49.36 192.38,47.11C191.31,45.84 190.91,45.25 191.21,45.43C191.31,45.49 191.79,45.49 192.3,45.41C193.35,45.25 195.56,45.29 197.03,45.47Z\"\n      android:fillColor=\"#FD755C\"/>\n  <path\n      android:pathData=\"M254.69,43.3C254.67,43.32 253.85,43.4 252.84,43.46C250.73,43.62 250.28,43.68 249.31,43.86C248.93,43.92 248.39,44.02 248.11,44.06C247.82,44.12 246.88,44.35 245.99,44.59C245.1,44.83 244.24,45.05 244.08,45.09C243.39,45.25 240.05,46.6 238.64,47.29L237.13,48.03L237.1,58.45C237.1,64.17 237.04,69.31 236.98,69.87C236.7,72.29 234.87,74.73 232.45,75.86C231.36,76.38 230.88,76.48 229.51,76.5C228.6,76.5 227.6,76.46 227.25,76.38C226.65,76.22 221.09,74.14 219.6,73.5C219.15,73.32 218.65,73.14 218.49,73.1C218.33,73.06 216.8,72.47 215.1,71.81C213.41,71.12 211.98,70.58 211.92,70.58C211.86,70.58 211.48,70.44 211.07,70.25C210.15,69.85 209.85,69.73 208.15,69.09C207.37,68.78 206.66,68.62 206.52,68.7C206.36,68.8 205.68,69.37 204.99,69.97C204.29,70.58 203.56,71.22 203.38,71.38C199.17,75.05 192.22,82.08 188.87,86.09C188.37,86.7 187.76,87.4 187.5,87.68C186.46,88.83 186.13,89.74 186.23,91.23C186.27,91.93 187.3,93.32 188.05,93.67C188.95,94.09 190.04,94.19 190.79,93.91C191.13,93.79 192.08,93.2 192.9,92.64C196.04,90.48 202.09,86.8 202.67,86.7C202.79,86.68 203.1,86.49 203.38,86.29C204.26,85.69 209.18,83.37 209.48,83.43C209.56,83.45 209.62,83.39 209.62,83.29C209.62,83.19 209.83,83.05 210.09,82.97C210.33,82.89 210.63,82.79 210.73,82.77C210.85,82.73 211.7,82.36 212.65,81.96C213.59,81.54 214.72,81.2 215.18,81.15C216.76,81.03 218.51,82.3 218.77,83.75C218.83,84.08 218.91,84.36 218.97,84.42C219.23,84.68 217.72,87.94 217.1,88.51C216.98,88.61 216.86,88.81 216.03,90.3C215.81,90.7 215.55,91.11 215.47,91.23C215.39,91.33 214.8,92.42 214.16,93.65C213.51,94.86 212.93,95.94 212.83,96.06C212.75,96.16 212.52,96.67 212.32,97.17C212.12,97.68 211.84,98.3 211.66,98.56C211.5,98.82 211.34,99.15 211.32,99.27C211.3,99.39 210.99,100.11 210.63,100.9C209.73,102.87 208.92,104.91 207.87,107.85C206.68,111.24 206.72,111.03 206.72,112.18C206.7,113.41 207.09,114.18 208.05,114.96C208.64,115.45 209.02,115.57 209.97,115.61C211.5,115.69 212.24,115.29 213.39,113.77C213.65,113.45 214.62,112.16 215.57,110.91C217.24,108.72 217.84,107.95 218.49,107.25C218.65,107.06 219.38,106.24 220.08,105.41C221.41,103.9 227.37,97.88 228.87,96.57C230.32,95.28 233.05,93.02 234.93,91.57C237.17,89.86 242.34,86.29 242.63,86.29C242.75,86.29 242.87,86.21 242.91,86.13C242.97,85.97 245.1,84.66 247.6,83.27C255.8,78.66 266.12,74.77 275.65,72.73C278.08,72.21 278.61,72.27 279.43,73.2C280.02,73.84 279.96,75.25 279.33,75.92C278.89,76.38 277.42,76.96 276.25,77.13C275.29,77.27 270.51,78.88 268.39,79.78C264.32,81.54 255.88,86.25 255.6,86.94C255.56,87.02 255.44,87.1 255.32,87.1C255.1,87.1 252.34,89.11 250.99,90.26C250.76,90.46 250.16,90.95 249.68,91.33C248.17,92.54 243.59,97.19 242.08,99.07C240.09,101.52 237.91,104.89 236.46,107.69C235.8,109 235.29,110.07 235.33,110.07C235.39,110.07 235.27,110.35 235.11,110.67C234.43,111.98 233.1,116.59 232.71,118.93C232.31,121.47 232.17,126.16 232.45,128.62C232.87,132.47 233.76,136.14 234.93,138.98C235.55,140.47 235.94,140.93 236.94,141.44C238.56,142.24 240.51,141.58 241.58,139.82C244.6,134.95 247.74,132.29 254.49,128.92C255.74,128.3 256.89,127.8 257.03,127.8C257.19,127.8 257.43,127.65 257.58,127.49C257.72,127.33 257.94,127.19 258.08,127.19C258.22,127.19 258.92,126.93 259.63,126.63C260.32,126.31 261.22,125.92 261.6,125.76C262.75,125.3 268.78,122.24 269.7,121.65C270.19,121.35 271.4,120.6 272.38,119.98C273.39,119.37 274.74,118.47 275.4,117.98C276.07,117.52 276.84,116.96 277.12,116.78C279.25,115.22 283.85,110.97 285.4,109.06C285.78,108.6 286.18,108.11 286.29,107.97C287.78,106.3 291.48,100.39 291.08,100.39C290.98,100.39 291.02,100.32 291.16,100.23C291.77,99.85 293.88,93.75 294.45,90.72C295.05,87.58 295.11,86.8 295.11,82.77C295.13,78.56 295.07,77.85 294.45,74.79C293.64,70.88 291.97,66.33 290.21,63.4C289.65,62.44 288.16,60.08 287.96,59.8C282.56,52.63 276.29,48.01 268.37,45.35C267.39,45.03 266.44,44.73 266.26,44.69C266.08,44.65 265.57,44.53 265.13,44.43C264.69,44.31 264.06,44.14 263.72,44.08C263.4,44.02 262.89,43.92 262.61,43.86C261.26,43.58 254.9,43.14 254.69,43.3ZM240.09,112.75C241.42,114.86 243.98,117.36 246.39,118.97C248.65,120.46 252.4,122.14 254.15,122.44C254.43,122.48 254.79,122.58 254.96,122.64C255.12,122.7 255.56,122.78 255.92,122.84C256.85,122.96 256.89,123.06 256.12,123.36C253.91,124.25 247.9,127.37 246.8,128.24C246.7,128.32 245.91,128.9 245.08,129.51C243.17,130.94 239.85,134.14 239.85,134.57C239.85,134.63 239.72,134.79 239.6,134.93C239.46,135.09 239.2,135.39 239.04,135.63C238.49,136.42 238.49,136.4 237.67,133.01C236.86,129.71 236.72,128.48 236.74,124.67C236.74,122.9 236.82,120.99 236.9,120.44C237.17,118.71 237.83,115.91 238.15,115.14C238.31,114.74 238.46,114.36 238.43,114.3C238.41,114.08 239.3,111.88 239.42,111.88C239.48,111.88 239.79,112.26 240.09,112.75Z\"\n      android:fillColor=\"#FD755C\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/profile_picture_placeholder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <shape android:shape=\"oval\">\n            <solid android:color=\"?attr/colorSecondary\" />\n        </shape>\n    </item>\n    <item\n        android:left=\"5dp\"\n        android:top=\"5dp\"\n        android:right=\"5dp\"\n        android:bottom=\"5dp\"\n        android:drawable=\"@drawable/ic_person_24\" />\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/progress_horizontal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:id=\"@android:id/background\">\n        <shape>\n            <corners android:radius=\"5dip\" />\n            <solid android:color=\"#19BDBDBD\" />\n        </shape>\n    </item>\n    <item android:id=\"@android:id/progress\">\n        <clip>\n            <shape>\n                <corners android:radius=\"5dip\" />\n                <solid android:color=\"#AACCCCCC\" />\n            </shape>\n        </clip>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/radial_edge_fade.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <gradient\n        android:startColor=\"@android:color/transparent\"\n        android:endColor=\"@color/edge_fade_color\"\n        android:gradientRadius=\"120%p\"\n        android:type=\"radial\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/rectangle_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <corners android:radius=\"5dp\" />\n\n    <stroke\n        android:width=\"1dp\"\n        android:color=\"?attr/colorButtonNormal\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/selectable_item_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"?attr/colorControlHighlight\">\n\n    <item android:id=\"@android:id/mask\">\n        <shape>\n            <solid android:color=\"@color/black\" />\n            <corners\n                android:bottomLeftRadius=\"8dp\"\n                android:bottomRightRadius=\"8dp\"\n                android:topLeftRadius=\"8dp\"\n                android:topRightRadius=\"8dp\" />\n        </shape>\n    </item>\n\n    <item>\n        <selector>\n            <item android:state_selected=\"true\">\n                <color android:color=\"?attr/colorControlHighlight\" />\n            </item>\n            <item android:state_activated=\"true\">\n                <color android:color=\"?attr/colorControlHighlight\" />\n            </item>\n        </selector>\n    </item>\n\n</ripple>"
  },
  {
    "path": "app/src/main/res/drawable/selector_home.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:drawable=\"@drawable/ic_home_24\"\n        android:state_selected=\"true\" />\n    <item\n        android:drawable=\"@drawable/ic_outline_home_24\"\n        android:state_selected=\"false\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/selector_library.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:drawable=\"@drawable/ic_view_list_24\"\n        android:state_selected=\"true\" />\n    <item\n        android:drawable=\"@drawable/ic_outline_view_list_24\"\n        android:state_selected=\"false\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/selector_profile.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:drawable=\"@drawable/ic_person_24\"\n        android:state_selected=\"true\" />\n    <item\n        android:drawable=\"@drawable/ic_outline_person_24\"\n        android:state_selected=\"false\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/selector_search.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:drawable=\"@drawable/ic_search_24\"\n        android:state_selected=\"true\" />\n    <item\n        android:drawable=\"@drawable/ic_search_24\"\n        android:state_selected=\"false\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/top_edge_fade.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <gradient\n        android:angle=\"90\"\n        android:startColor=\"@android:color/transparent\"\n        android:endColor=\"@color/edge_fade_color\"\n        android:type=\"linear\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/top_edge_fade_surface.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <gradient\n        android:angle=\"90\"\n        android:startColor=\"@android:color/transparent\"\n        android:endColor=\"?attr/colorSurface\"\n        android:type=\"linear\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/translucent_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/translucent_overlay\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/widget_rounded_rect.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <solid android:color=\"#FFFFFF\" />\n    <corners android:radius=\"20dp\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable-night/progress_horizontal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:id=\"@android:id/background\">\n        <shape>\n            <corners android:radius=\"5dip\" />\n            <solid android:color=\"#19F5F5F5\" />\n        </shape>\n    </item>\n    <item android:id=\"@android:id/progress\">\n        <clip>\n            <shape>\n                <corners android:radius=\"5dip\" />\n                <solid android:color=\"#4FCCCCCC\" />\n            </shape>\n        </clip>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/layout/activity_authentication.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/coordinator_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.authentication.AuthenticationActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:navigationIcon=\"@drawable/ic_arrow_back_24\"\n            app:navigationContentDescription=\"@string/action_back\"\n            style=\"@style/Widget.Material3.Toolbar.OnSurface\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.core.widget.NestedScrollView\n        android:id=\"@+id/nsv_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fillViewport=\"true\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <RelativeLayout\n            android:paddingHorizontal=\"@dimen/activity_horizontal_margin\"\n            android:paddingVertical=\"20dp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <ImageView\n                android:layout_width=\"144dp\"\n                android:layout_height=\"144dp\"\n                android:layout_marginBottom=\"20dp\"\n                android:src=\"@drawable/ic_launcher_foreground\"\n                android:layout_centerHorizontal=\"true\"\n                android:layout_above=\"@id/layout_login_form\"\n                android:contentDescription=\"@null\" />\n\n            <LinearLayout\n                android:id=\"@+id/layout_login_form\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                android:layout_centerInParent=\"true\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_horizontal\"\n                    android:text=\"@string/title_login\"\n                    android:textAppearance=\"?attr/textAppearanceHeadlineMedium\"\n                    android:textColor=\"?attr/colorSecondary\"\n                    android:layout_marginBottom=\"24dp\" />\n\n                <TextView\n                    android:id=\"@+id/tv_additional_info\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_horizontal\"\n                    android:visibility=\"gone\"\n                    tools:text=\"Your access token has expired. Please log in again.\"\n                    tools:visibility=\"visible\"\n                    android:drawablePadding=\"4dp\"\n                    app:drawableStartCompat=\"@drawable/ic_outline_info_24\" />\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/field_username\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"24dp\"\n                    android:hint=\"@string/prompt_email\"\n                    style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox\">\n\n                    <com.google.android.material.textfield.TextInputEditText\n                        android:id=\"@+id/input_username\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:inputType=\"textEmailAddress\"\n                        android:selectAllOnFocus=\"true\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/field_password\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"10dp\"\n                    android:hint=\"@string/prompt_password\"\n                    app:endIconMode=\"password_toggle\"\n                    style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox\">\n\n                    <com.google.android.material.textfield.TextInputEditText\n                        android:id=\"@+id/input_password\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:imeOptions=\"actionDone\"\n                        android:inputType=\"textPassword\"\n                        android:selectAllOnFocus=\"true\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n                <com.google.android.material.button.MaterialButton\n                    android:id=\"@+id/btn_login\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"start\"\n                    android:layout_marginTop=\"16dp\"\n                    android:layout_marginBottom=\"32dp\"\n                    android:enabled=\"false\"\n                    android:text=\"@string/action_sign_in\" />\n\n            </LinearLayout>\n\n            <TextView\n                android:id=\"@+id/tv_create_account\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:textAppearance=\"?attr/textAppearanceBodyMedium\"\n                android:text=\"@string/login_no_account\"\n                android:gravity=\"center\"\n                android:layout_alignParentBottom=\"true\"\n                android:layout_centerHorizontal=\"true\"\n                android:layout_marginVertical=\"16dp\"/>\n\n        </RelativeLayout>\n\n    </androidx.core.widget.NestedScrollView>\n\n    <LinearLayout\n        android:id=\"@+id/layout_loading\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"fill\"\n        android:orientation=\"vertical\"\n        android:gravity=\"center\"\n        android:background=\"@drawable/translucent_background\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:visibility=\"gone\"\n        tools:visibility=\"gone\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/status_logging_in\"\n            android:layout_gravity=\"center\" />\n\n        <ProgressBar\n            android:id=\"@+id/loading\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layout_marginTop=\"64dp\"\n            android:layout_marginBottom=\"64dp\" />\n\n    </LinearLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"io.github.drumber.kitsune.ui.main.MainActivity\">\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/nav_host_fragment\"\n        android:name=\"androidx.navigation.fragment.NavHostFragment\"\n        app:defaultNavHost=\"true\"\n        app:navGraph=\"@navigation/main_nav_graph\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_above=\"@id/bottom_navigation\" />\n\n    <com.google.android.material.bottomnavigation.BottomNavigationView\n        android:id=\"@+id/bottom_navigation\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentBottom=\"true\"\n        app:labelVisibilityMode=\"labeled\"\n        app:menu=\"@menu/main_nav_menu\"/>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_photo_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <import type=\"io.github.drumber.kitsune.util.ui.BindingAdapter\" />\n    </data>\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/black\"\n        tools:context=\".ui.photoview.PhotoViewActivity\">\n\n        <app.futured.hauler.HaulerView\n            android:id=\"@+id/hauler_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:dragDismissDistance=\"120dp\"\n            app:dragElasticity=\"1.0\"\n            app:dragDismissScale=\"0.9\">\n\n            <io.github.drumber.kitsune.ui.component.PhotoViewNestedScrollView\n                android:id=\"@+id/nested_scroll_view\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:fillViewport=\"true\">\n\n                <FrameLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\">\n\n                    <com.github.chrisbanes.photoview.PhotoView\n                        android:id=\"@+id/photo_view\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\" />\n\n                </FrameLayout>\n\n            </io.github.drumber.kitsune.ui.component.PhotoViewNestedScrollView>\n\n        </app.futured.hauler.HaulerView>\n\n        <View\n            android:id=\"@+id/status_bar_background\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_gravity=\"top\"\n            android:background=\"@color/translucent_system_overlay\" />\n\n        <LinearLayout\n            android:id=\"@+id/fullscreen_content_controls\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom|center_horizontal\"\n            android:orientation=\"horizontal\"\n            android:paddingVertical=\"5dp\"\n            android:paddingHorizontal=\"10dp\"\n            android:background=\"@color/translucent_system_overlay\">\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/btn_close\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                app:icon=\"@drawable/ic_close_24\"\n                app:iconTint=\"@color/white\"\n                app:tooltip=\"@{@string/action_close}\"\n                style=\"?attr/materialIconButtonStyle\" />\n\n            <Space\n                android:layout_width=\"0dp\"\n                android:layout_height=\"1dp\"\n                android:layout_weight=\"1\" />\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/btn_save\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                app:icon=\"@drawable/ic_save_alt_24\"\n                app:iconTint=\"@color/white\"\n                app:tooltip=\"@{@string/action_save_in_gallery}\"\n                style=\"?attr/materialIconButtonStyle\" />\n\n            <Space\n                android:layout_width=\"0dp\"\n                android:layout_height=\"1dp\"\n                android:layout_weight=\"1\" />\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/btn_open\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                app:icon=\"@drawable/ic_open_in_browser_24\"\n                app:iconTint=\"@color/white\"\n                app:tooltip=\"@{@string/action_open_in_browser}\"\n                style=\"?attr/materialIconButtonStyle\" />\n\n        </LinearLayout>\n\n        <com.google.android.material.progressindicator.LinearProgressIndicator\n            android:id=\"@+id/progress_indicator\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:indeterminate=\"true\"\n            app:hideAnimationBehavior=\"outward\"\n            app:trackThickness=\"1dp\"\n            android:layout_gravity=\"top\"/>\n\n    </FrameLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/custom_edit_text_preference.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingHorizontal=\"?attr/dialogPreferredPadding\"\n    android:paddingTop=\"?attr/dialogPreferredPadding\">\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:id=\"@+id/text_input_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/custom_number_spinner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:minWidth=\"@dimen/edit_library_min_width\"\n    android:background=\"@drawable/rectangle_background\"\n    android:orientation=\"horizontal\">\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/btn_decrement\"\n        style=\"?attr/materialIconButtonStyle\"\n        android:layout_width=\"48dp\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_remove_24\"\n        app:iconGravity=\"textStart\" />\n\n    <View\n        android:layout_width=\"1dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginVertical=\"8dp\"\n        android:background=\"?attr/colorButtonNormal\" />\n\n    <Space\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\" />\n\n    <com.google.android.material.textfield.TextInputEditText\n        android:id=\"@+id/field_count\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"1\"\n        android:layout_marginHorizontal=\"10dp\"\n        android:background=\"@android:color/transparent\"\n        android:gravity=\"center\"\n        android:inputType=\"number|numberDecimal|numberSigned\"\n        android:minWidth=\"30dp\"\n        android:textAppearance=\"?attr/textAppearanceBodyLarge\"\n        tools:text=\"8\" />\n\n    <TextView\n        android:id=\"@+id/tv_suffix\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginEnd=\"10dp\"\n        android:gravity=\"center\"\n        android:textAppearance=\"?attr/textAppearanceBodyLarge\"\n        tools:text=\"/ 12\" />\n\n    <Space\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\" />\n\n    <View\n        android:layout_width=\"1dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginVertical=\"8dp\"\n        android:background=\"?attr/colorButtonNormal\" />\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/btn_increment\"\n        style=\"?attr/materialIconButtonStyle\"\n        android:layout_width=\"48dp\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_add_24\"\n        app:iconGravity=\"textStart\" />\n\n    <LinearLayout\n        android:id=\"@+id/layout_action\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <View\n            android:layout_width=\"1dp\"\n            android:layout_height=\"match_parent\"\n            android:background=\"?attr/colorButtonNormal\" />\n\n        <com.google.android.material.button.MaterialButton\n            android:id=\"@+id/btn_action\"\n            style=\"@style/Widget.Material3.Button.TextButton\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:paddingHorizontal=\"10dp\"\n            tools:text=\"@string/action_start\"\n            android:textAllCaps=\"false\" />\n\n    </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/custom_preference_switch.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.materialswitch.MaterialSwitch\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/switchWidget\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"/>"
  },
  {
    "path": "app/src/main/res/layout/fragment_app_logs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/coordinator_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.settings.AppLogsFragment\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:liftOnScroll=\"true\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:title=\"@string/nav_app_logs\"\n            app:menu=\"@menu/logs_menu\"\n            app:navigationIcon=\"@drawable/ic_arrow_back_24\"\n            app:navigationContentDescription=\"@string/action_back\"\n            app:layout_scrollFlags=\"noScroll\"\n            style=\"@style/Widget.Material3.Toolbar\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n   <androidx.core.widget.NestedScrollView\n       android:id=\"@+id/nested_scroll_view\"\n       android:layout_width=\"match_parent\"\n       android:layout_height=\"match_parent\"\n       app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n       android:clipToPadding=\"false\"\n       android:fillViewport=\"true\">\n\n       <FrameLayout\n           android:layout_width=\"match_parent\"\n           android:layout_height=\"match_parent\">\n\n           <TextView\n               android:id=\"@+id/tv_log_messages\"\n               android:layout_width=\"match_parent\"\n               android:layout_height=\"match_parent\"\n               android:textIsSelectable=\"true\"\n               android:gravity=\"top\" />\n\n           <TextView\n               android:id=\"@+id/tv_no_logs\"\n               android:layout_width=\"wrap_content\"\n               android:layout_height=\"wrap_content\"\n               android:layout_gravity=\"center\"\n               android:textAppearance=\"?attr/textAppearanceHeadlineSmall\"\n               android:text=\"@string/app_logs_no_data\"\n               android:visibility=\"gone\"/>\n\n           <ProgressBar\n               android:id=\"@+id/progress_bar\"\n               android:layout_width=\"wrap_content\"\n               android:layout_height=\"wrap_content\"\n               android:layout_gravity=\"center\"\n               android:visibility=\"visible\" />\n\n       </FrameLayout>\n\n   </androidx.core.widget.NestedScrollView>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_categories.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/coordinator_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:liftOnScroll=\"true\">\n\n        <com.google.android.material.appbar.CollapsingToolbarLayout\n            android:id=\"@+id/collapsing_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/collapsingToolbarLayoutLargeSize\"\n            app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n            style=\"?attr/collapsingToolbarLayoutLargeStyle\">\n\n            <com.google.android.material.appbar.MaterialToolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:title=\"@string/title_categories\"\n                app:navigationIcon=\"@drawable/ic_close_24\"\n                app:navigationContentDescription=\"@string/action_close\"\n                app:layout_collapseMode=\"pin\"\n                style=\"@style/Widget.Material3.Toolbar\" />\n\n        </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.core.widget.NestedScrollView\n        android:id=\"@+id/nested_scroll_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipToPadding=\"false\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <FrameLayout\n            android:id=\"@+id/tree_view_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n    </androidx.core.widget.NestedScrollView>\n\n    <include\n        android:id=\"@+id/layout_loading\"\n        layout=\"@layout/layout_resource_loading\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:visibility=\"gone\"\n        tools:visibility=\"visible\"/>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_characters.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    tools:context=\".ui.medialist.MediaListFragment\">\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/coordinator_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/app_bar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:liftOnScroll=\"true\">\n\n            <com.google.android.material.appbar.CollapsingToolbarLayout\n                android:id=\"@+id/collapsing_toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/collapsingToolbarLayoutLargeSize\"\n                app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n                style=\"?attr/collapsingToolbarLayoutLargeStyle\">\n\n                <com.google.android.material.appbar.MaterialToolbar\n                    android:id=\"@+id/toolbar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"?attr/actionBarSize\"\n                    app:title=\"@string/title_characters\"\n                    app:navigationIcon=\"@drawable/ic_arrow_back_24\"\n                    app:navigationContentDescription=\"@string/action_back\"\n                    app:layout_collapseMode=\"pin\"\n                    style=\"@style/Widget.Material3.Toolbar\" />\n\n            </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv_media\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n            android:orientation=\"horizontal\"\n            android:clipToPadding=\"false\"\n            tools:listitem=\"@layout/item_character\"/>\n\n        <include\n            android:id=\"@+id/layout_loading\"\n            layout=\"@layout/layout_resource_loading\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:visibility=\"gone\"\n            tools:visibility=\"visible\"/>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_details.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout 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\n    <data>\n        <import type=\"android.text.TextUtils\" />\n        <import type=\"io.github.drumber.kitsune.data.presentation.model.media.Anime\" />\n\n        <variable\n            name=\"data\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.media.Media\" />\n\n        <variable\n            name=\"libraryEntry\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\" />\n    </data>\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        tools:context=\".ui.main.MainActivity\">\n\n        <androidx.coordinatorlayout.widget.CoordinatorLayout\n            android:id=\"@+id/coordinator_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <com.google.android.material.appbar.AppBarLayout\n                android:id=\"@+id/app_bar_layout\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                app:liftOnScroll=\"true\">\n\n                <com.google.android.material.appbar.CollapsingToolbarLayout\n                    android:id=\"@+id/collapsing_toolbar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"?attr/collapsingToolbarLayoutLargeSize\"\n                    app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n                    app:maxLines=\"3\"\n                    style=\"@style/Widget.Material3.CollapsingToolbar.Large\">\n\n                    <FrameLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        app:layout_collapseMode=\"parallax\"\n                        app:layout_collapseParallaxMultiplier=\"0.7\">\n\n                        <ImageView\n                            android:id=\"@+id/iv_cover\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:contentDescription=\"@null\" />\n\n                        <View\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"125dp\"\n                            android:layout_gravity=\"bottom\"\n                            android:background=\"@drawable/bottom_edge_fade_surface\" />\n\n                        <View\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"120dp\"\n                            android:layout_marginTop=\"-10dp\"\n                            android:layout_gravity=\"top\"\n                            android:background=\"@drawable/top_edge_fade_surface\" />\n\n                    </FrameLayout>\n\n                    <com.google.android.material.appbar.MaterialToolbar\n                        android:id=\"@+id/toolbar\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"?attr/actionBarSize\"\n                        app:layout_collapseMode=\"pin\"\n                        app:menu=\"@menu/details_menu\"\n                        app:navigationContentDescription=\"@string/action_back\"\n                        app:navigationIcon=\"@drawable/ic_arrow_back_24\"\n                        app:title=\"@{data.title}\"\n                        tools:title=\"Some Title\" />\n\n                    <com.google.android.material.progressindicator.LinearProgressIndicator\n                        android:id=\"@+id/progress_indicator\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"bottom\"\n                        android:indeterminate=\"true\"\n                        app:hideAnimationBehavior=\"outward\"\n                        app:layout_collapseMode=\"pin\"\n                        app:trackThickness=\"1dp\" />\n\n                </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n            </com.google.android.material.appbar.AppBarLayout>\n\n            <androidx.core.widget.NestedScrollView\n                android:id=\"@+id/nsv_content\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n                <LinearLayout\n                    android:id=\"@+id/content\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_marginTop=\"10dp\"\n                    android:clipToPadding=\"false\"\n                    android:orientation=\"vertical\"\n                    android:padding=\"15dp\">\n\n                    <RelativeLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\">\n\n                        <ImageView\n                            android:id=\"@+id/iv_thumbnail\"\n                            android:layout_width=\"@dimen/details_thumbnail_width\"\n                            android:layout_height=\"@dimen/details_thumbnail_height\"\n                            android:layout_alignParentStart=\"true\"\n                            android:layout_alignParentTop=\"true\"\n                            android:contentDescription=\"@null\"\n                            android:transitionName=\"@string/details_poster_transition_name\"\n                            tools:src=\"@drawable/ic_insert_photo_48\" />\n\n                        <TextView\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_below=\"@id/iv_thumbnail\"\n                            android:layout_alignParentStart=\"true\"\n                            android:layout_marginTop=\"10dp\"\n                            android:text=\"@{data.publishingYearText(context) + ` • ` + data.subtypeFormatted}\"\n                            tools:text=\"2002 • TV\" />\n\n                        <LinearLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:layout_alignParentEnd=\"true\"\n                            android:layout_centerVertical=\"true\"\n                            android:layout_marginStart=\"10dp\"\n                            android:layout_toEndOf=\"@id/iv_thumbnail\"\n                            android:orientation=\"vertical\">\n\n                            <include\n                                layout=\"@layout/section_details_stats\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_marginBottom=\"10dp\"\n                                app:data=\"@{data}\" />\n\n                            <Button\n                                android:id=\"@+id/btn_manage_library\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/library_action_add\"\n                                android:textAllCaps=\"false\" />\n\n                            <Button\n                                android:id=\"@+id/btn_edit_library_entry\"\n                                style=\"@style/Widget.Material3.Button.OutlinedButton\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/library_action_edit\"\n                                android:textAllCaps=\"false\"\n                                app:isVisible=\"@{libraryEntry != null}\"\n                                tools:visibility=\"visible\" />\n\n                        </LinearLayout>\n\n                    </RelativeLayout>\n\n                    <HorizontalScrollView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginTop=\"10dp\"\n                        android:layout_marginBottom=\"10dp\"\n                        android:requiresFadingEdge=\"horizontal\"\n                        android:scrollbars=\"none\">\n\n                        <com.google.android.material.chip.ChipGroup\n                            android:id=\"@+id/chip_group_categories\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:clipToPadding=\"false\"\n                            app:singleLine=\"true\" />\n\n                    </HorizontalScrollView>\n\n                    <include\n                        layout=\"@layout/section_details_description\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginBottom=\"25dp\"\n                        app:data=\"@{data}\"\n                        app:isVisible=\"@{!TextUtils.isEmpty(data.description)}\" />\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:layout_marginBottom=\"10dp\"\n                            android:text=\"@string/title_details\"\n                            android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n                        <include\n                            android:id=\"@+id/section_details_info\"\n                            layout=\"@layout/section_details_info\"\n                            app:data=\"@{data}\" />\n\n                    </LinearLayout>\n\n                    <Button\n                        android:id=\"@+id/btn_media_units\"\n                        style=\"@style/Widget.Material3.Button.OutlinedButton\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:text=\"@{data instanceof Anime ? @string/title_episodes : @string/title_chapters}\"\n                        android:textAllCaps=\"false\" />\n\n                    <Button\n                        android:id=\"@+id/btn_characters\"\n                        style=\"@style/Widget.Material3.Button.OutlinedButton\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginTop=\"10dp\"\n                        android:text=\"@string/title_characters\"\n                        android:textAllCaps=\"false\" />\n\n                    <include\n                        layout=\"@layout/section_details_trailer\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginTop=\"20dp\"\n                        app:coverUrl=\"@{data.trailerCoverUrl}\"\n                        app:isVisible=\"@{!TextUtils.isEmpty(data.trailerUrl)}\"\n                        app:videoUrl=\"@{data.trailerUrl}\" />\n\n                    <LinearLayout\n                        android:id=\"@+id/layout_franchise\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginTop=\"20dp\"\n                        android:orientation=\"vertical\"\n                        app:isVisible=\"@{data.hasMediaRelationships()}\">\n\n                        <TextView\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginBottom=\"10dp\"\n                            android:text=\"@string/title_more_franchise\"\n                            android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n                        <androidx.recyclerview.widget.RecyclerView\n                            android:id=\"@+id/rv_franchise\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:clipToPadding=\"false\"\n                            android:orientation=\"horizontal\"\n                            android:paddingVertical=\"5dp\"\n                            android:requiresFadingEdge=\"horizontal\"\n                            android:transitionGroup=\"true\"\n                            app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\" />\n\n                    </LinearLayout>\n\n                    <LinearLayout\n                        android:id=\"@+id/layout_streamer\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginTop=\"10dp\"\n                        android:orientation=\"vertical\"\n                        app:isVisible=\"@{data.hasStreamingLinks()}\">\n\n                        <TextView\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginBottom=\"10dp\"\n                            android:text=\"@string/title_streamer\"\n                            android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n                        <androidx.recyclerview.widget.RecyclerView\n                            android:id=\"@+id/rv_streamer\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:clipToPadding=\"false\"\n                            android:orientation=\"horizontal\"\n                            android:paddingVertical=\"5dp\"\n                            android:requiresFadingEdge=\"horizontal\"\n                            app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\" />\n\n                    </LinearLayout>\n\n                    <LinearLayout\n                        android:id=\"@+id/layout_ratings\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginTop=\"10dp\"\n                        android:orientation=\"vertical\"\n                        app:isVisible=\"@{data.hasRatingFrequencies()}\">\n\n                        <RelativeLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginBottom=\"5dp\"\n                            android:orientation=\"horizontal\">\n\n                            <TextView\n                                android:id=\"@+id/tv_title_ratings\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_alignParentTop=\"true\"\n                                android:layout_alignParentStart=\"true\"\n                                android:layout_toStartOf=\"@id/btn_rating_type_menu\"\n                                android:text=\"@string/title_ratings\"\n                                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n                            <TextView\n                                android:id=\"@+id/tv_calculated_average_rating\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_alignParentStart=\"true\"\n                                android:layout_below=\"@id/tv_title_ratings\"\n                                android:layout_marginTop=\"5dp\"\n                                android:textAppearance=\"?attr/textAppearanceLabelLarge\"\n                                app:drawableStartCompat=\"@drawable/ic_bar_chart_16\"\n                                android:drawablePadding=\"6dp\"\n                                tools:text=\"6.75\" />\n\n                            <TextView\n                                android:id=\"@+id/tv_calculated_average_rating_max\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_toEndOf=\"@id/tv_calculated_average_rating\"\n                                android:layout_alignBaseline=\"@id/tv_calculated_average_rating\"\n                                android:layout_marginStart=\"4dp\"\n                                android:textAppearance=\"?attr/textAppearanceLabelMedium\"\n                                tools:text=\"/ 5.0\" />\n\n                            <com.google.android.material.button.MaterialButton\n                                android:id=\"@+id/btn_rating_type_menu\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_marginStart=\"10dp\"\n                                android:layout_alignParentTop=\"true\"\n                                android:layout_alignParentEnd=\"true\"\n                                app:icon=\"@drawable/ic_more_vert\"\n                                app:iconTint=\"?attr/colorControlNormal\"\n                                style=\"?attr/materialIconButtonStyle\" />\n\n                        </RelativeLayout>\n\n                        <com.github.mikephil.charting.charts.BarChart\n                            android:id=\"@+id/chart_ratings\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"200dp\" />\n\n                    </LinearLayout>\n\n                </LinearLayout>\n\n            </androidx.core.widget.NestedScrollView>\n\n        </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n    </FrameLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_edit_library_entry.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.main.MainActivity\">\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/coordinator_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_above=\"@id/card_bottom_bar\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_alignParentTop=\"true\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/app_bar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:liftOnScroll=\"true\">\n\n            <com.google.android.material.appbar.MaterialToolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:layout_scrollFlags=\"noScroll\"\n                app:navigationContentDescription=\"@string/action_close\"\n                app:navigationIcon=\"@drawable/ic_close_24\"\n                app:title=\"@string/title_edit_library_entry\"\n                style=\"@style/Widget.Material3.Toolbar\" />\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <androidx.core.widget.NestedScrollView\n            android:id=\"@+id/nested_scroll_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"10dp\"\n                android:orientation=\"vertical\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"@dimen/edit_library_entry_img_height\"\n                    android:layout_marginBottom=\"10dp\"\n                    android:orientation=\"horizontal\">\n\n                    <ImageView\n                        android:id=\"@+id/iv_thumbnail\"\n                        android:layout_width=\"@dimen/edit_library_entry_img_width\"\n                        android:layout_height=\"@dimen/edit_library_entry_img_height\"\n                        android:contentDescription=\"@null\"\n                        tools:src=\"@drawable/ic_insert_photo_48\" />\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        android:layout_marginStart=\"10dp\"\n                        android:orientation=\"vertical\">\n\n                        <TextView\n                            android:id=\"@+id/tv_title\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:paddingBottom=\"5dp\"\n                            android:ellipsize=\"end\"\n                            android:maxLines=\"2\"\n                            android:textAppearance=\"?attr/textAppearanceBodyLarge\"\n                            tools:text=\"Some Media Title - That can be long\" />\n\n                        <TextView\n                            android:id=\"@+id/tv_media_info\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:maxLines=\"1\"\n                            tools:text=\"2002 • TV\" />\n\n                    </LinearLayout>\n\n                </LinearLayout>\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/menu_library_status\"\n                    style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\"\n                    android:layout_marginTop=\"12dp\"\n                    android:hint=\"@string/library_edit_status\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\">\n\n                    <AutoCompleteTextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:inputType=\"none\"\n                        tools:ignore=\"LabelFor\"\n                        tools:text=\"Currently Watching\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/field_rating\"\n                    style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\"\n                    android:hint=\"@string/library_edit_rating\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"12dp\"\n                    app:endIconDrawable=\"@drawable/ic_star_outline_24\"\n                    app:endIconTint=\"?attr/colorControlNormal\"\n                    app:endIconMode=\"custom\">\n\n                    <AutoCompleteTextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:focusable=\"false\"\n                        android:inputType=\"none\"\n                        tools:ignore=\"LabelFor\"\n                        tools:text=\"No Rating\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/menu_privacy\"\n                    style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\"\n                    android:hint=\"@string/library_edit_privacy\"\n                    android:layout_marginTop=\"12dp\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\">\n\n                    <AutoCompleteTextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:inputType=\"none\"\n                        tools:ignore=\"LabelFor\"\n                        tools:text=\"Public\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n                <com.google.android.material.divider.MaterialDivider\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginVertical=\"24dp\"/>\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:maxLines=\"1\"\n                        android:ellipsize=\"end\"\n                        android:text=\"@string/library_edit_progress\" />\n\n                    <io.github.drumber.kitsune.ui.component.CustomNumberSpinner\n                        android:id=\"@+id/spinner_progress\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:minWidth=\"@dimen/edit_library_min_width\"\n                        app:suffixMode=\"maxValue\"\n                        tools:maxValue=\"12\" />\n\n                </LinearLayout>\n\n                <!-- Manga only -->\n                <LinearLayout\n                    android:id=\"@+id/layout_volumes\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"12dp\"\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:maxLines=\"1\"\n                        android:ellipsize=\"end\"\n                        android:text=\"@string/library_edit_volumes\"/>\n\n                    <io.github.drumber.kitsune.ui.component.CustomNumberSpinner\n                        android:id=\"@+id/spinner_volumes\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:minWidth=\"@dimen/edit_library_min_width\"\n                        app:suffixMode=\"maxValue\"\n                        tools:maxValue=\"7\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"12dp\"\n                    android:orientation=\"horizontal\">\n\n                    <TextView\n                        android:id=\"@+id/tv_reconsume_label\"\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:layout_weight=\"1\"\n                        android:maxLines=\"1\"\n                        android:ellipsize=\"end\"\n                        android:layout_marginEnd=\"8dp\"\n                        android:text=\"@string/library_edit_rewatch_count\" />\n\n                    <io.github.drumber.kitsune.ui.component.CustomNumberSpinner\n                        android:id=\"@+id/spinner_reconsume\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:minWidth=\"@dimen/edit_library_min_width\"\n                        app:actionText=\"@string/action_start\"\n                        app:enableAction=\"true\" />\n\n                </LinearLayout>\n\n                <com.google.android.material.divider.MaterialDivider\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginVertical=\"24dp\"/>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"horizontal\">\n\n                    <com.google.android.material.textfield.TextInputLayout\n                        android:id=\"@+id/field_started\"\n                        style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\"\n                        android:hint=\"@string/library_edit_started\"\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_weight=\"1\"\n                        app:endIconDrawable=\"@drawable/ic_calendar_month_24\"\n                        app:endIconMode=\"custom\">\n\n                        <AutoCompleteTextView\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:focusable=\"false\"\n                            android:inputType=\"none\"\n                            tools:ignore=\"LabelFor\"\n                            tools:text=\"01/02/2022\" />\n\n                    </com.google.android.material.textfield.TextInputLayout>\n\n                    <com.google.android.material.button.MaterialButton\n                        android:id=\"@+id/btn_clear_started\"\n                        style=\"?attr/materialIconButtonStyle\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        app:icon=\"@drawable/ic_close_24\"\n                        app:iconTint=\"?attr/colorControlNormal\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"12dp\"\n                    android:orientation=\"horizontal\">\n\n                    <com.google.android.material.textfield.TextInputLayout\n                        android:id=\"@+id/field_finished\"\n                        style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\"\n                        android:hint=\"@string/library_edit_finished\"\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_weight=\"1\"\n                        app:endIconDrawable=\"@drawable/ic_calendar_month_24\"\n                        app:endIconMode=\"custom\">\n\n                        <AutoCompleteTextView\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:focusable=\"false\"\n                            android:inputType=\"none\"\n                            tools:ignore=\"LabelFor\"\n                            tools:text=\"31/02/2022\" />\n\n                    </com.google.android.material.textfield.TextInputLayout>\n\n                    <com.google.android.material.button.MaterialButton\n                        android:id=\"@+id/btn_clear_finished\"\n                        style=\"?attr/materialIconButtonStyle\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        app:icon=\"@drawable/ic_close_24\"\n                        app:iconTint=\"?attr/colorControlNormal\" />\n\n                </LinearLayout>\n\n                <com.google.android.material.divider.MaterialDivider\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginVertical=\"24dp\"/>\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/field_notes\"\n                    style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox\"\n                    android:hint=\"@string/library_edit_notes\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\">\n\n                    <com.google.android.material.textfield.TextInputEditText\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:gravity=\"top\"\n                        android:inputType=\"text|textMultiLine\"\n                        android:minLines=\"5\"\n                        tools:text=\"My Personal Notes\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n            </LinearLayout>\n\n        </androidx.core.widget.NestedScrollView>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n    <com.google.android.material.card.MaterialCardView\n        android:id=\"@+id/card_bottom_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_alignParentBottom=\"true\"\n        app:cardCornerRadius=\"0dp\"\n        style=\"?attr/materialCardViewFilledStyle\"\n        app:cardBackgroundColor=\"?attr/colorSurfaceContainer\">\n\n        <LinearLayout\n            android:id=\"@+id/layout_bottom_bar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"10dp\"\n            android:clipToPadding=\"false\"\n            android:orientation=\"horizontal\">\n\n            <Button\n                android:id=\"@+id/btn_remove_entry\"\n                style=\"?attr/materialIconButtonOutlinedStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"5dp\"\n                android:contentDescription=\"@string/library_action_remove\"\n                android:textAllCaps=\"false\"\n                app:icon=\"@drawable/ic_delete_24\" />\n\n            <Space\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\" />\n\n            <Button\n                android:id=\"@+id/btn_save_changes\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"5dp\"\n                android:text=\"@string/action_save_changes\"\n                android:textAllCaps=\"false\"\n                app:icon=\"@drawable/ic_save_24\" />\n\n        </LinearLayout>\n\n    </com.google.android.material.card.MaterialCardView>\n\n    <FrameLayout\n        android:id=\"@+id/layout_loading\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_centerInParent=\"true\"\n        android:elevation=\"20dp\"\n        android:background=\"@drawable/translucent_background\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:visibility=\"gone\"\n        tools:visibility=\"gone\">\n\n        <ProgressBar\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\" />\n\n    </FrameLayout>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_edit_profile.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:liftOnScroll=\"true\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            style=\"@style/Widget.Material3.Toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:layout_scrollFlags=\"noScroll\"\n            app:navigationContentDescription=\"@string/action_close\"\n            app:navigationIcon=\"@drawable/ic_close_24\"\n            app:title=\"@string/title_edit_profile\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.core.widget.NestedScrollView\n        android:id=\"@+id/nested_scroll_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:layout_margin=\"10dp\"\n            android:orientation=\"vertical\">\n\n            <com.google.android.material.card.MaterialCardView\n                android:id=\"@+id/card_cover\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"150dp\"\n                style=\"@style/Widget.Kitsune.CardView.Surface\">\n\n                <ImageView\n                    android:id=\"@+id/iv_cover\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:contentDescription=\"@string/profile_cover_image_description\"\n                    tools:src=\"@drawable/cover_placeholder\" />\n\n                <ImageView\n                    android:id=\"@+id/iv_cover_add_image\"\n                    android:layout_width=\"30dp\"\n                    android:layout_height=\"30dp\"\n                    android:src=\"@drawable/ic_add_a_photo_24\"\n                    android:layout_gravity=\"center\"\n                    app:tint=\"?attr/colorOnPrimary\"\n                    android:contentDescription=\"@null\" />\n\n            </com.google.android.material.card.MaterialCardView>\n\n            <com.google.android.material.card.MaterialCardView\n                android:id=\"@+id/card_avatar\"\n                android:layout_width=\"100dp\"\n                android:layout_height=\"100dp\"\n                android:layout_gravity=\"center_horizontal\"\n                android:layout_marginTop=\"-50dp\"\n                android:layout_marginBottom=\"32dp\"\n                app:cardCornerRadius=\"50dp\"\n                app:strokeColor=\"?attr/colorSurface\"\n                app:strokeWidth=\"2dp\">\n\n                <de.hdodenhof.circleimageview.CircleImageView\n                    android:id=\"@+id/iv_avatar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:contentDescription=\"@string/profile_avatar_image_description\"\n                    tools:src=\"@drawable/profile_picture_placeholder\" />\n\n            </com.google.android.material.card.MaterialCardView>\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_profile_links\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"15dp\"\n                app:chipSpacingVertical=\"5dp\">\n\n                <com.google.android.material.chip.Chip\n                    android:id=\"@+id/chip_add_profile_link\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:checkable=\"false\"\n                    android:text=\"@string/action_add_profile_link\"\n                    app:chipIcon=\"@drawable/ic_add_24\"\n                    style=\"@style/Widget.Material3.Chip.Assist\"/>\n\n            </com.google.android.material.chip.ChipGroup>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/field_location\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:hint=\"@string/profile_data_location\"\n                app:endIconMode=\"clear_text\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:inputType=\"text\"\n                    android:maxLines=\"1\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/field_birthday\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"18dp\"\n                android:hint=\"@string/profile_data_birthday\"\n                app:endIconDrawable=\"@drawable/ic_close_24\"\n                app:endIconMode=\"custom\"\n                style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\">\n\n                <AutoCompleteTextView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:focusable=\"false\"\n                    android:inputType=\"none\"\n                    tools:ignore=\"LabelFor\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/menu_gender\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"18dp\"\n                android:hint=\"@string/profile_data_gender\"\n                style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\">\n\n                <AutoCompleteTextView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:inputType=\"none\"\n                    tools:text=\"Female\"\n                    tools:ignore=\"LabelFor\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/field_custom_gender\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"2dp\"\n                android:hint=\"@string/profile_gender_custom_hint\"\n                app:endIconMode=\"clear_text\"\n                android:visibility=\"gone\"\n                tools:visibility=\"visible\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:inputType=\"text\"\n                    android:maxLines=\"1\"/>\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/menu_waifu\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"18dp\"\n                android:hint=\"@string/profile_waifu_or_husbando\"\n                style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\">\n\n                <AutoCompleteTextView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:inputType=\"none\"\n                    tools:ignore=\"LabelFor\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/field_search_waifu\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"2dp\"\n                android:hint=\"@string/profile_find_waifu\"\n                app:endIconDrawable=\"@drawable/ic_search_24\"\n                app:endIconMode=\"custom\"\n                style=\"@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu\">\n\n                <AutoCompleteTextView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:focusable=\"false\"\n                    android:inputType=\"none\"\n                    tools:ignore=\"LabelFor\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/field_bio\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"18dp\"\n                android:hint=\"@string/profile_data_bio\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:gravity=\"top\"\n                    android:inputType=\"text|textMultiLine\"\n                    android:minLines=\"5\"\n                    tools:text=\"About me...\\nMulti\\nLine\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <Button\n                android:id=\"@+id/btn_update_profile\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"14dp\"\n                android:text=\"@string/action_update_profile\" />\n\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n\n    <com.google.android.material.search.SearchView\n        android:id=\"@+id/character_search_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:hint=\"@string/hint_search_characters\"\n        tools:visibility=\"gone\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv_character_results\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:clipToPadding=\"false\" />\n\n    </com.google.android.material.search.SearchView>\n\n    <FrameLayout\n        android:id=\"@+id/layout_loading\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:elevation=\"20dp\"\n        android:background=\"@drawable/translucent_background\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:visibility=\"gone\"\n        tools:visibility=\"gone\">\n\n        <ProgressBar\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\" />\n\n    </FrameLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_filter_facet.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/coordinator_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:liftOnScroll=\"true\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:layout_scrollFlags=\"noScroll\"\n            app:navigationIcon=\"@drawable/ic_close_24\"\n            app:navigationContentDescription=\"@string/action_close\"\n            app:menu=\"@menu/filter_facet_menu\"\n            app:title=\"@string/title_filter\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.core.widget.NestedScrollView\n        android:id=\"@+id/nsv_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fillViewport=\"true\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:id=\"@+id/tv_kind\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"10dp\"\n                android:text=\"@string/filter_kind\"\n                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:id=\"@+id/rv_kind\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:nestedScrollingEnabled=\"false\"\n                tools:itemCount=\"2\"\n                tools:listitem=\"@layout/item_facet\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                android:padding=\"10dp\"\n                android:layout_marginTop=\"8dp\">\n\n                <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"@string/filter_year\"\n                    android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n                <TextView\n                    android:id=\"@+id/tv_year_value\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    tools:text=\"1862 - 2024\" />\n\n            </LinearLayout>\n\n            <com.google.android.material.slider.RangeSlider\n                android:id=\"@+id/slider_year\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"16dp\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                android:padding=\"10dp\"\n                android:layout_marginTop=\"8dp\">\n\n                <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"@string/filter_avg_rating\"\n                    android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n                <TextView\n                    android:id=\"@+id/tv_avg_rating_value\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    tools:text=\"5% - 100%\" />\n\n            </LinearLayout>\n\n            <com.google.android.material.slider.RangeSlider\n                android:id=\"@+id/slider_avg_rating\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"16dp\" />\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"10dp\"\n                android:layout_marginTop=\"8dp\"\n                android:text=\"@string/title_categories\"\n                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n            <com.google.android.material.card.MaterialCardView\n                android:id=\"@+id/card_categories\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/listPreferredItemHeightSmall\"\n                app:cardCornerRadius=\"0dp\"\n                style=\"@style/Widget.Kitsune.CardView.Surface\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:orientation=\"horizontal\"\n                    android:paddingHorizontal=\"16dp\"\n                    android:gravity=\"center_vertical\">\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:layout_marginEnd=\"16dp\"\n                        android:ellipsize=\"end\"\n                        android:maxLines=\"1\"\n                        android:textAppearance=\"?attr/textAppearanceBodyLarge\"\n                        android:textSize=\"16sp\"\n                        android:text=\"@string/filter_select_categories\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_categories_counter\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:background=\"@drawable/badge_background\"\n                        android:elevation=\"2dp\"\n                        android:minHeight=\"22dp\"\n                        android:minWidth=\"22dp\"\n                        android:layout_marginEnd=\"16dp\"\n                        android:paddingLeft=\"5dp\"\n                        android:paddingRight=\"5dp\"\n                        android:maxLines=\"1\"\n                        android:gravity=\"center\"\n                        android:visibility=\"gone\"\n                        tools:text=\"99+\"\n                        tools:visibility=\"visible\" />\n\n                    <ImageView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:src=\"@drawable/ic_arrow_forward_24\"\n                        android:contentDescription=\"@null\" />\n\n                </LinearLayout>\n\n            </com.google.android.material.card.MaterialCardView>\n\n            <TextView\n                android:id=\"@+id/tv_season\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"10dp\"\n                android:layout_marginTop=\"8dp\"\n                android:text=\"@string/filter_season\"\n                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:id=\"@+id/rv_season\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:nestedScrollingEnabled=\"false\"\n                tools:itemCount=\"4\"\n                tools:listitem=\"@layout/item_facet\" />\n\n            <TextView\n                android:id=\"@+id/tv_subtype\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"10dp\"\n                android:layout_marginTop=\"8dp\"\n                android:text=\"@string/filter_subtype\"\n                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n            <io.github.drumber.kitsune.ui.component.ExpandableLayout\n                android:id=\"@+id/wrapper_subtype\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                app:expanded=\"false\"\n                app:min_height=\"300dp\">\n\n                <androidx.recyclerview.widget.RecyclerView\n                    android:id=\"@+id/rv_subtype\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:paddingBottom=\"40dp\"\n                    android:clipToPadding=\"false\"\n                    tools:itemCount=\"5\"\n                    tools:listitem=\"@layout/item_facet\" />\n\n                <FrameLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"bottom\"\n                    android:paddingTop=\"10dp\"\n                    android:background=\"@drawable/bottom_edge_fade_surface\">\n\n                    <com.google.android.material.button.MaterialButton\n                        android:id=\"@+id/btn_expand_subtype\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:background=\"@android:color/transparent\"\n                        app:cornerRadius=\"0dp\"\n                        android:textAllCaps=\"false\"\n                        tools:text=\"@string/action_show_more\"\n                        style=\"?attr/borderlessButtonStyle\" />\n\n                </FrameLayout>\n\n            </io.github.drumber.kitsune.ui.component.ExpandableLayout>\n\n            <TextView\n                android:id=\"@+id/tv_streamers\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"10dp\"\n                android:layout_marginTop=\"8dp\"\n                android:text=\"@string/filter_streamers\"\n                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n            <io.github.drumber.kitsune.ui.component.ExpandableLayout\n                android:id=\"@+id/wrapper_streamers\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                app:expanded=\"false\"\n                app:min_height=\"300dp\">\n\n                <androidx.recyclerview.widget.RecyclerView\n                    android:id=\"@+id/rv_streamers\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:paddingBottom=\"40dp\"\n                    android:clipToPadding=\"false\"\n                    tools:itemCount=\"5\"\n                    tools:listitem=\"@layout/item_facet\" />\n\n                <FrameLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"bottom\"\n                    android:paddingTop=\"10dp\"\n                    android:background=\"@drawable/bottom_edge_fade_surface\">\n\n                    <Button\n                        android:id=\"@+id/btn_expand_streamers\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        app:cornerRadius=\"0dp\"\n                        android:background=\"@android:color/transparent\"\n                        android:textAllCaps=\"false\"\n                        tools:text=\"@string/action_show_more\"\n                        style=\"?attr/borderlessButtonStyle\" />\n\n                </FrameLayout>\n\n            </io.github.drumber.kitsune.ui.component.ExpandableLayout>\n\n            <TextView\n                android:id=\"@+id/tv_age_rating\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"10dp\"\n                android:layout_marginTop=\"8dp\"\n                android:text=\"@string/filter_age_rating\"\n                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:id=\"@+id/rv_age_rating\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:nestedScrollingEnabled=\"false\"\n                tools:itemCount=\"4\"\n                tools:listitem=\"@layout/item_facet\" />\n\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n\n    <include\n        android:id=\"@+id/layout_search_provider_status\"\n        layout=\"@layout/layout_search_provider_loading\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:visibility=\"gone\"\n        tools:visibility=\"gone\"/>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_home_explore.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:paddingBottom=\"10dp\"\n    android:divider=\"@drawable/explore_section_divider\"\n    android:showDividers=\"middle\">\n\n    <include\n        android:id=\"@+id/section_trending\"\n        layout=\"@layout/section_main_explore\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <include\n        android:id=\"@+id/section_top_airing\"\n        layout=\"@layout/section_main_explore\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <include\n        android:id=\"@+id/section_top_upcoming\"\n        layout=\"@layout/section_main_explore\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <include\n        android:id=\"@+id/section_highest_rated\"\n        layout=\"@layout/section_main_explore\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <include\n        android:id=\"@+id/section_most_popular\"\n        layout=\"@layout/section_main_explore\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_library.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    tools:context=\".ui.library.LibraryFragment\"\n    android:id=\"@+id/coordinator_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:fitsSystemWindows=\"true\"\n        app:liftOnScrollTargetViewId=\"@id/rv_library_entries\"\n        app:liftOnScroll=\"true\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:title=\"@string/nav_library\"\n            app:layout_scrollFlags=\"scroll|snap|enterAlways\"\n            style=\"@style/Widget.Material3.Toolbar\" />\n\n        <HorizontalScrollView\n            android:id=\"@+id/scroll_view_filter\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:scrollbars=\"none\"\n            app:layout_scrollFlags=\"scroll|snap|enterAlways\">\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_filter\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:paddingHorizontal=\"10dp\"\n                android:clipToPadding=\"false\"\n                app:singleLine=\"true\">\n\n                <com.google.android.material.chip.Chip\n                    android:id=\"@+id/chip_media_kind\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    app:closeIcon=\"@drawable/ic_arrow_drop_down_24\"\n                    app:closeIconEnabled=\"true\"\n                    android:text=\"@string/library_kind_all\"/>\n\n                <com.google.android.material.chip.Chip\n                    android:id=\"@+id/chip_current\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/library_status_watching\"\n                    style=\"@style/Widget.Material3.Chip.Filter\"/>\n\n                <com.google.android.material.chip.Chip\n                    android:id=\"@+id/chip_planned\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/library_status_planned\"\n                    style=\"@style/Widget.Material3.Chip.Filter\"/>\n\n                <com.google.android.material.chip.Chip\n                    android:id=\"@+id/chip_completed\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/library_status_completed\"\n                    style=\"@style/Widget.Material3.Chip.Filter\"/>\n\n                <com.google.android.material.chip.Chip\n                    android:id=\"@+id/chip_on_hold\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/library_status_on_hold\"\n                    style=\"@style/Widget.Material3.Chip.Filter\"/>\n\n                <com.google.android.material.chip.Chip\n                    android:id=\"@+id/chip_dropped\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/library_status_dropped\"\n                    style=\"@style/Widget.Material3.Chip.Filter\"/>\n\n            </com.google.android.material.chip.ChipGroup>\n\n        </HorizontalScrollView>\n\n        <com.google.android.material.progressindicator.LinearProgressIndicator\n            android:id=\"@+id/progress_indicator\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:indeterminate=\"true\"\n            app:hideAnimationBehavior=\"outward\"\n            app:trackThickness=\"1dp\"\n            android:visibility=\"invisible\"\n            android:layout_gravity=\"bottom\"\n            app:layout_collapseMode=\"pin\"/>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n        android:id=\"@+id/swipe_refresh_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv_library_entries\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:clipToPadding=\"false\"\n            android:transitionGroup=\"true\"\n            tools:listitem=\"@layout/item_library_entry\" />\n\n    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n    <include\n        android:id=\"@+id/layout_loading\"\n        layout=\"@layout/layout_resource_loading\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:visibility=\"gone\"\n        tools:visibility=\"gone\"/>\n\n    <androidx.core.widget.NestedScrollView\n        android:id=\"@+id/nsv_not_logged_in\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        android:visibility=\"gone\"\n        tools:visibility=\"gone\">\n\n        <LinearLayout\n            android:id=\"@+id/layout_not_logged_in\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:orientation=\"vertical\"\n            android:padding=\"20dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"10dp\"\n                android:textAppearance=\"@style/TextAppearance.MaterialComponents.Headline6\"\n                android:text=\"@string/library_not_logged_in_title\" />\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/library_not_logged_in_text\" />\n\n            <Button\n                android:id=\"@+id/btn_login\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"20dp\"\n                android:layout_marginBottom=\"20dp\"\n                android:textAllCaps=\"false\"\n                android:text=\"@string/action_log_in_to_kitsu\" />\n\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    tools:context=\"io.github.drumber.kitsune.ui.main.MainFragment\">\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/coordinator_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/app_bar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:fitsSystemWindows=\"true\"\n            app:liftOnScrollTargetViewId=\"@id/nsv_content\"\n            app:liftOnScroll=\"true\">\n\n            <com.google.android.material.appbar.MaterialToolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:layout_scrollFlags=\"scroll|snap\"\n                app:titleCentered=\"true\"\n                app:title=\"@string/app_name\" />\n\n            <com.google.android.material.tabs.TabLayout\n                android:id=\"@+id/tab_layout_explore\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                style=\"@style/Widget.Material3.TabLayout.OnSurface\" />\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n            android:id=\"@+id/swipe_refresh_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n            <androidx.core.widget.NestedScrollView\n                android:id=\"@+id/nsv_content\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:clipToPadding=\"false\">\n\n                <androidx.viewpager2.widget.ViewPager2\n                    android:id=\"@+id/view_pager_explore\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\" />\n\n            </androidx.core.widget.NestedScrollView>\n\n        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n        <include\n            android:id=\"@+id/layout_loading\"\n            layout=\"@layout/layout_resource_loading\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:visibility=\"gone\"\n            tools:visibility=\"gone\"/>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_media_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    tools:context=\".ui.medialist.MediaListFragment\">\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/coordinator_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/app_bar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:liftOnScroll=\"true\">\n\n            <com.google.android.material.appbar.CollapsingToolbarLayout\n                android:id=\"@+id/collapsing_toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/collapsingToolbarLayoutLargeSize\"\n                app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n                style=\"?attr/collapsingToolbarLayoutLargeStyle\">\n\n                <com.google.android.material.appbar.MaterialToolbar\n                    android:id=\"@+id/toolbar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"?attr/actionBarSize\"\n                    app:title=\"@string/app_name\"\n                    app:navigationIcon=\"@drawable/ic_arrow_back_24\"\n                    app:navigationContentDescription=\"@string/action_back\"\n                    app:layout_collapseMode=\"pin\"\n                    style=\"@style/Widget.Material3.Toolbar\" />\n\n            </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv_media\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n            android:orientation=\"horizontal\"\n            android:clipToPadding=\"false\"\n            android:transitionGroup=\"true\"\n            tools:listitem=\"@layout/item_media\"/>\n\n        <include\n            android:id=\"@+id/layout_loading\"\n            layout=\"@layout/layout_resource_loading\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:visibility=\"gone\"\n            tools:visibility=\"visible\"/>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_os_libraries.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/coordinator_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.main.MainActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:liftOnScroll=\"true\">\n\n        <com.google.android.material.appbar.CollapsingToolbarLayout\n            android:id=\"@+id/collapsing_toolbar\"\n            style=\"?attr/collapsingToolbarLayoutLargeStyle\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/collapsingToolbarLayoutLargeSize\"\n            app:layout_scrollFlags=\"scroll|exitUntilCollapsed\">\n\n            <com.google.android.material.appbar.MaterialToolbar\n                android:id=\"@+id/toolbar\"\n                style=\"@style/Widget.Material3.Toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:navigationContentDescription=\"@string/action_back\"\n                app:navigationIcon=\"@drawable/ic_arrow_back_24\"\n                app:title=\"@string/nav_os_libraries\"\n                app:layout_collapseMode=\"pin\"/>\n\n        </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/os_libraries_fragment_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"/>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_preference.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/coordinator_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:liftOnScroll=\"true\">\n\n        <com.google.android.material.appbar.CollapsingToolbarLayout\n            android:id=\"@+id/collapsing_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/collapsingToolbarLayoutLargeSize\"\n            app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n            style=\"?attr/collapsingToolbarLayoutLargeStyle\">\n\n            <com.google.android.material.appbar.MaterialToolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:title=\"@string/nav_settings\"\n                app:navigationIcon=\"@drawable/ic_arrow_back_24\"\n                app:navigationContentDescription=\"@string/action_back\"\n                app:layout_collapseMode=\"pin\"\n                style=\"@style/Widget.Material3.Toolbar\" />\n\n        </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <FrameLayout\n        android:id=\"@android:id/list_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipToPadding=\"false\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n    <FrameLayout\n        android:id=\"@+id/loading_overlay\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:visibility=\"gone\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:alpha=\"0.7\"\n        android:background=\"?attr/colorSurface\">\n\n        <ProgressBar\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\" />\n\n    </FrameLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_profile.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <import type=\"io.github.drumber.kitsune.util.DataUtil\" />\n        <import type=\"android.text.TextUtils\" />\n        <variable\n            name=\"user\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.user.User\" />\n    </data>\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/coordinator_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        tools:context=\".ui.main.MainActivity\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/app_bar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:liftOnScrollTargetViewId=\"@id/nsv_content\"\n            app:liftOnScroll=\"true\">\n\n            <com.google.android.material.appbar.CollapsingToolbarLayout\n                android:id=\"@+id/collapsing_toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/collapsingToolbarLayoutLargeSize\"\n                app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n                style=\"@style/Widget.Material3.CollapsingToolbar.Large\">\n\n                <FrameLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    app:layout_collapseMode=\"parallax\"\n                    app:layout_collapseParallaxMultiplier=\"0.7\">\n\n                    <ImageView\n                        android:id=\"@+id/iv_cover\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        android:contentDescription=\"@null\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"80dp\"\n                        android:layout_gravity=\"bottom\"\n                        android:background=\"@drawable/bottom_edge_fade_surface\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"80dp\"\n                        android:layout_marginTop=\"-15dp\"\n                        android:layout_gravity=\"top\"\n                        android:background=\"@drawable/top_edge_fade_surface\" />\n\n                </FrameLayout>\n\n                <com.google.android.material.appbar.MaterialToolbar\n                    android:id=\"@+id/toolbar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"?attr/actionBarSize\"\n                    android:background=\"@android:color/transparent\"\n                    app:logo=\"@drawable/profile_picture_placeholder\"\n                    app:logoAdjustViewBounds=\"true\"\n                    app:title=\"@{user.name ?? @string/not_logged_in}\"\n                    app:layout_collapseMode=\"pin\"\n                    app:menu=\"@menu/profile_menu\"\n                    app:titleMarginStart=\"10dp\"\n                    tools:title=\"Some Username\"/>\n\n            </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n            android:id=\"@+id/swipe_refresh_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n            <androidx.core.widget.NestedScrollView\n                android:id=\"@+id/nsv_content\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:clipToPadding=\"false\"\n                android:fillViewport=\"true\">\n\n                <FrameLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\">\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        app:isVisible=\"@{user != null}\"\n                        tools:visibility=\"visible\"\n                        android:orientation=\"vertical\">\n\n                        <LinearLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:orientation=\"horizontal\"\n                            android:layout_marginHorizontal=\"10dp\">\n\n                            <com.google.android.material.button.MaterialButton\n                                android:id=\"@+id/btnFollowing\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                style=\"?attr/borderlessButtonStyle\"\n                                android:textAppearance=\"?attr/textAppearanceLabelMedium\"\n                                android:text=\"@{@string/profile_data_following(user.followingCount ?? 0)}\"\n                                tools:text=\"12 Following\"/>\n\n                            <com.google.android.material.button.MaterialButton\n                                android:id=\"@+id/btnFollowers\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                style=\"?attr/borderlessButtonStyle\"\n                                android:textAppearance=\"?attr/textAppearanceLabelMedium\"\n                                android:text=\"@{@string/profile_data_followers(user.followersCount ?? 0)}\"\n                                tools:text=\"39 Followers\"/>\n\n                        </LinearLayout>\n\n                        <com.google.android.material.card.MaterialCardView\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_margin=\"10dp\"\n                            style=\"?attr/materialCardViewOutlinedStyle\">\n\n                            <LinearLayout\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:padding=\"@dimen/card_inner_padding\"\n                                android:orientation=\"vertical\">\n\n                                <TextView\n                                    android:layout_width=\"wrap_content\"\n                                    android:layout_height=\"wrap_content\"\n                                    android:layout_marginBottom=\"10dp\"\n                                    android:textAppearance=\"?attr/textAppearanceHeadlineSmall\"\n                                    android:text=\"@string/title_about_me\" />\n\n                                <TextView\n                                    android:layout_width=\"wrap_content\"\n                                    android:layout_height=\"wrap_content\"\n                                    android:layout_marginBottom=\"10dp\"\n                                    app:isVisible=\"@{!TextUtils.isEmpty(user.about)}\"\n                                    android:text=\"@{user.about}\"\n                                    tools:text=\"Personal about me text.\" />\n\n                                <TableLayout\n                                    android:layout_width=\"match_parent\"\n                                    android:layout_height=\"wrap_content\"\n                                    android:stretchColumns=\"0,1\">\n\n                                    <include\n                                        layout=\"@layout/item_details_info_row\"\n                                        app:icon=\"@{@drawable/ic_person_24}\"\n                                        app:title=\"@{@string/profile_data_gender}\"\n                                        app:value=\"@{DataUtil.getGenderString(user.gender, context) ?? @string/profile_data_private}\" />\n\n                                    <include\n                                        layout=\"@layout/item_details_info_row\"\n                                        app:icon=\"@{@drawable/ic_location_24}\"\n                                        app:title=\"@{@string/profile_data_location}\"\n                                        app:value=\"@{!TextUtils.isEmpty(user.location) ? user.location : @string/profile_data_private}\" />\n\n                                    <include\n                                        layout=\"@layout/item_details_info_row\"\n                                        app:icon=\"@{@drawable/ic_cake_24}\"\n                                        app:title=\"@{@string/profile_data_birthday}\"\n                                        app:value=\"@{DataUtil.formatDate(user.birthday) ?? @string/profile_data_private}\" />\n\n                                    <include\n                                        layout=\"@layout/item_details_info_row\"\n                                        app:icon=\"@{@drawable/ic_calendar_24}\"\n                                        app:title=\"@{@string/profile_data_join_date}\"\n                                        app:value=\"@{DataUtil.formatUserJoinDate(user.createdAt, context) ?? @string/profile_data_private}\" />\n\n                                    <include\n                                        layout=\"@layout/item_details_info_row\"\n                                        android:id=\"@+id/layout_waifu_row\"\n                                        app:isVisible=\"@{user.waifu != null}\"\n                                        app:title=\"@{user.waifuOrHusbando}\"\n                                        app:value=\"@{user.waifu.name}\" />\n\n                                </TableLayout>\n\n                            </LinearLayout>\n\n                        </com.google.android.material.card.MaterialCardView>\n\n                        <HorizontalScrollView\n                            android:id=\"@+id/scroll_view_profile_links\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginHorizontal=\"10dp\"\n                            android:requiresFadingEdge=\"horizontal\"\n                            android:scrollbars=\"none\">\n\n                            <com.google.android.material.chip.ChipGroup\n                                android:id=\"@+id/chip_group_profile_links\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:clipToPadding=\"false\"\n                                app:singleLine=\"true\">\n\n                            </com.google.android.material.chip.ChipGroup>\n\n                        </HorizontalScrollView>\n\n                        <com.google.android.material.card.MaterialCardView\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_margin=\"10dp\"\n                            style=\"?attr/materialCardViewOutlinedStyle\">\n\n                            <LinearLayout\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:orientation=\"vertical\">\n\n                                <com.google.android.material.tabs.TabLayout\n                                    android:id=\"@+id/tab_layout_stats\"\n                                    android:layout_width=\"match_parent\"\n                                    android:layout_height=\"wrap_content\"\n                                    style=\"@style/Widget.Material3.TabLayout.OnSurface\" />\n\n                                <androidx.viewpager2.widget.ViewPager2\n                                    android:id=\"@+id/view_pager_stats\"\n                                    android:layout_width=\"match_parent\"\n                                    android:layout_height=\"wrap_content\" />\n\n                            </LinearLayout>\n\n                        </com.google.android.material.card.MaterialCardView>\n\n                        <LinearLayout\n                            android:id=\"@+id/layout_favorite_anime\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:orientation=\"vertical\"\n                            android:layout_margin=\"10dp\"\n                            android:visibility=\"gone\"\n                            tools:visibility=\"visible\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/title_favorite_anime\"\n                                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\"\n                                android:layout_marginBottom=\"10dp\" />\n\n                            <androidx.recyclerview.widget.RecyclerView\n                                android:id=\"@+id/rv_favorite_anime\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:paddingVertical=\"5dp\"\n                                android:requiresFadingEdge=\"horizontal\"\n                                android:clipToPadding=\"false\"\n                                android:orientation=\"horizontal\"\n                                android:transitionGroup=\"true\"\n                                app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\" />\n\n                        </LinearLayout>\n\n                        <LinearLayout\n                            android:id=\"@+id/layout_favorite_manga\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:orientation=\"vertical\"\n                            android:layout_margin=\"10dp\"\n                            android:visibility=\"gone\"\n                            tools:visibility=\"visible\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/title_favorite_manga\"\n                                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\"\n                                android:layout_marginBottom=\"10dp\" />\n\n                            <androidx.recyclerview.widget.RecyclerView\n                                android:id=\"@+id/rv_favorite_manga\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:paddingVertical=\"5dp\"\n                                android:requiresFadingEdge=\"horizontal\"\n                                android:clipToPadding=\"false\"\n                                android:orientation=\"horizontal\"\n                                android:transitionGroup=\"true\"\n                                app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\" />\n\n                        </LinearLayout>\n\n                        <LinearLayout\n                            android:id=\"@+id/layout_favorite_characters\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:orientation=\"vertical\"\n                            android:layout_margin=\"10dp\"\n                            android:visibility=\"gone\"\n                            tools:visibility=\"visible\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/title_favorite_characters\"\n                                android:textAppearance=\"?attr/textAppearanceHeadlineSmall\"\n                                android:layout_marginBottom=\"10dp\" />\n\n                            <androidx.recyclerview.widget.RecyclerView\n                                android:id=\"@+id/rv_favorite_characters\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:paddingVertical=\"5dp\"\n                                android:requiresFadingEdge=\"horizontal\"\n                                android:clipToPadding=\"false\"\n                                android:orientation=\"horizontal\"\n                                app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\" />\n\n                        </LinearLayout>\n\n                    </LinearLayout>\n\n                    <!-- Not Logged In Message -->\n                    <LinearLayout\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:padding=\"20dp\"\n                        android:orientation=\"vertical\"\n                        tools:visibility=\"gone\"\n                        app:isVisible=\"@{user == null}\">\n\n                        <TextView\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginBottom=\"8dp\"\n                            android:textAppearance=\"?attr/textAppearanceHeadlineSmall\"\n                            android:text=\"@string/profile_not_logged_in_title\" />\n\n                        <TextView\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:textAppearance=\"?attr/textAppearanceBodyMedium\"\n                            android:text=\"@string/profile_not_logged_in_text\" />\n\n                        <Button\n                            android:id=\"@+id/btn_login\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginVertical=\"20dp\"\n                            android:text=\"@string/action_log_in_to_kitsu\"\n                            android:textAllCaps=\"false\" />\n\n                    </LinearLayout>\n\n                </FrameLayout>\n\n            </androidx.core.widget.NestedScrollView>\n\n        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_search.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    tools:context=\".ui.search.SearchFragment\">\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/coordinator_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipChildren=\"false\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/app_bar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:translationZ=\"1dp\"\n            android:outlineSpotShadowColor=\"@android:color/transparent\"\n            android:outlineAmbientShadowColor=\"@android:color/transparent\"\n            app:liftOnScroll=\"false\"\n            app:elevation=\"0dp\"\n            android:background=\"@android:color/transparent\"\n            tools:targetApi=\"p\">\n\n            <FrameLayout\n                android:id=\"@+id/search_wrapper\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                app:layout_scrollFlags=\"scroll|enterAlways|snap\"\n                android:background=\"@android:color/transparent\">\n\n                <com.google.android.material.card.MaterialCardView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"10dp\"\n                    app:cardCornerRadius=\"50dp\"\n                    app:cardElevation=\"6dp\"\n                    style=\"@style/Widget.Material3.CardView.Elevated\">\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"2dp\"\n                        android:clipToPadding=\"false\"\n                        android:clipChildren=\"false\"\n                        android:orientation=\"horizontal\">\n\n                        <androidx.appcompat.widget.AppCompatImageButton\n                            android:id=\"@+id/btn_search\"\n                            android:layout_width=\"24dp\"\n                            android:layout_height=\"24dp\"\n                            android:layout_marginStart=\"16dp\"\n                            android:src=\"@drawable/ic_search_24\"\n                            app:tint=\"?attr/colorControlNormal\"\n                            android:background=\"?attr/selectableItemBackgroundBorderless\"\n                            android:layout_gravity=\"center\"\n                            android:contentDescription=\"@string/hint_search\" />\n\n                        <androidx.appcompat.widget.SearchView\n                            android:id=\"@+id/search_view\"\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_weight=\"1\"\n                            app:defaultQueryHint=\"@string/hint_search\"\n                            app:searchIcon=\"@null\"\n                            style=\"@style/Widget.Kitsune.SearchView\" />\n\n                        <androidx.appcompat.widget.AppCompatImageButton\n                            android:id=\"@+id/btn_filter\"\n                            android:layout_width=\"24dp\"\n                            android:layout_height=\"24dp\"\n                            android:layout_marginEnd=\"16dp\"\n                            android:src=\"@drawable/ic_filter_24\"\n                            app:tint=\"?attr/colorControlNormal\"\n                            android:background=\"?attr/selectableItemBackgroundBorderless\"\n                            android:layout_gravity=\"center\"\n                            android:contentDescription=\"@string/title_filter\" />\n\n                    </LinearLayout>\n\n                </com.google.android.material.card.MaterialCardView>\n\n            </FrameLayout>\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv_media\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n            android:orientation=\"horizontal\"\n            android:clipToPadding=\"false\"\n            tools:listitem=\"@layout/item_media\"/>\n\n        <include\n            android:id=\"@+id/layout_loading\"\n            layout=\"@layout/layout_resource_loading\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:visibility=\"gone\"\n            tools:visibility=\"visible\"/>\n\n        <include\n            android:id=\"@+id/layout_search_provider_status\"\n            layout=\"@layout/layout_search_provider_loading\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:visibility=\"gone\"\n            tools:visibility=\"gone\"/>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_web_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.webview.WebViewFragment\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:titleTextAppearance=\"?attr/textAppearanceTitleMedium\"\n            app:subtitleTextAppearance=\"?attr/textAppearanceTitleSmall\"\n            app:navigationIcon=\"@drawable/ic_close_24\"\n            app:menu=\"@menu/webview_menu\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <FrameLayout\n        android:id=\"@+id/webViewWrapper\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\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/loadingIndicator\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:indeterminate=\"true\" />\n\n    </FrameLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_category_node.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:background=\"?attr/selectableItemBackground\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.divider.MaterialDivider\n        android:id=\"@+id/divider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"10dp\">\n\n        <ImageView\n            android:id=\"@+id/iv_expand\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:contentDescription=\"@null\"\n            android:src=\"@drawable/ic_keyboard_arrow_down_24\" />\n\n        <TextView\n            android:id=\"@+id/tv_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_toEndOf=\"@id/iv_expand\"\n            android:layout_toStartOf=\"@id/layout_control\"\n            android:layout_marginHorizontal=\"10dp\"\n            android:layout_centerVertical=\"true\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\"\n            tools:text=\"Some Name\"/>\n\n        <LinearLayout\n            android:id=\"@+id/layout_control\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_centerVertical=\"true\">\n\n            <TextView\n                android:id=\"@+id/tv_counter\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@drawable/badge_background\"\n                android:elevation=\"2dp\"\n                android:minHeight=\"22dp\"\n                android:minWidth=\"22dp\"\n                android:layout_marginEnd=\"5dp\"\n                android:paddingLeft=\"5dp\"\n                android:paddingRight=\"5dp\"\n                android:maxLines=\"1\"\n                android:gravity=\"center\"\n                android:visibility=\"gone\"\n                tools:text=\"99+\"\n                tools:visibility=\"visible\" />\n\n            <com.google.android.material.checkbox.MaterialCheckBox\n                android:id=\"@+id/checkbox\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"/>\n\n        </LinearLayout>\n\n    </RelativeLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_character.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_marginHorizontal=\"10dp\"\n    android:layout_marginVertical=\"5dp\"\n    style=\"?attr/materialCardViewOutlinedStyle\">\n    \n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/episode_card_img_height\">\n\n        <ImageView\n            android:id=\"@+id/iv_character\"\n            android:layout_width=\"@dimen/episode_card_img_width\"\n            android:layout_height=\"match_parent\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:contentDescription=\"@null\"\n            tools:src=\"@drawable/ic_insert_photo_48\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:layout_toEndOf=\"@id/iv_character\"\n            android:layout_toStartOf=\"@id/iv_actor\"\n            android:layout_centerVertical=\"true\"\n            android:paddingVertical=\"@dimen/card_inner_padding\"\n            android:paddingHorizontal=\"8dp\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:id=\"@+id/tv_character_name\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"start\"\n                android:textAppearance=\"?attr/textAppearanceBodyMedium\"\n                android:ellipsize=\"end\"\n                android:textIsSelectable=\"true\"\n                tools:text=\"Character Name\" />\n\n            <Space\n                android:layout_width=\"10dp\"\n                android:layout_height=\"0dp\" />\n\n            <TextView\n                android:id=\"@+id/tv_actor_name\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"end\"\n                android:textAppearance=\"?attr/textAppearanceBodyMedium\"\n                android:ellipsize=\"end\"\n                android:textIsSelectable=\"true\"\n                tools:text=\"Actor Name\" />\n\n        </LinearLayout>\n\n        <ImageView\n            android:id=\"@+id/iv_actor\"\n            android:layout_width=\"@dimen/episode_card_img_width\"\n            android:layout_height=\"match_parent\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:contentDescription=\"@null\"\n            tools:src=\"@drawable/ic_insert_photo_48\" />\n        \n    </RelativeLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/item_character_filter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:paddingHorizontal=\"10dp\"\n    android:paddingVertical=\"5dp\">\n\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/chip_language\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"end\"\n        app:closeIcon=\"@drawable/ic_arrow_drop_down_24\"\n        app:closeIconEnabled=\"true\"\n        tools:text=\"Japanese\"/>\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_character_search_result.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?attr/selectableItemBackground\"\n    android:padding=\"12dp\">\n\n    <de.hdodenhof.circleimageview.CircleImageView\n        android:id=\"@+id/iv_character\"\n        android:layout_width=\"50dp\"\n        android:layout_height=\"50dp\"\n        android:layout_centerVertical=\"true\"\n        android:layout_alignParentStart=\"true\"\n        tools:src=\"@drawable/character_placeholder\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginVertical=\"8dp\"\n        android:layout_marginHorizontal=\"18dp\"\n        android:layout_centerVertical=\"true\"\n        android:layout_toEndOf=\"@id/iv_character\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/tv_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?attr/textAppearanceBodyLarge\"\n            android:ellipsize=\"end\"\n            tools:text=\"Character Name\"/>\n\n        <TextView\n            android:id=\"@+id/tv_media\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"5dp\"\n            android:maxLines=\"1\"\n            android:textAppearance=\"?attr/textAppearanceCaption\"\n            android:ellipsize=\"end\"\n            tools:text=\"Media\"/>\n\n    </LinearLayout>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_details_info_row.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <variable\n            name=\"title\"\n            type=\"CharSequence\" />\n        <variable\n            name=\"value\"\n            type=\"CharSequence\" />\n        <variable\n            name=\"icon\"\n            type=\"android.graphics.drawable.Drawable\" />\n    </data>\n\n    <TableRow>\n        <LinearLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"5dp\"\n            android:paddingEnd=\"5dp\"\n            android:paddingBottom=\"5dp\"\n            android:orientation=\"horizontal\">\n\n            <ImageView\n                android:id=\"@+id/iv_icon\"\n                android:layout_width=\"20dp\"\n                android:layout_height=\"20dp\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_marginEnd=\"10dp\"\n                android:src=\"@{icon}\"\n                app:isVisible=\"@{icon != null}\"\n                android:contentDescription=\"@null\"\n                tools:visibility=\"gone\"\n                tools:src=\"@drawable/ic_favorite_24\" />\n\n            <TextView\n                android:id=\"@+id/tv_title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@{title}\"\n                android:layout_gravity=\"center_vertical\"\n                android:ellipsize=\"end\"\n                tools:text=\"Row Title\" />\n\n        </LinearLayout>\n\n        <TextView\n            android:id=\"@+id/tv_value\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:text=\"@{value}\"\n            android:paddingTop=\"5dp\"\n            android:paddingStart=\"5dp\"\n            android:paddingBottom=\"5dp\"\n            android:ellipsize=\"end\"\n            android:textIsSelectable=\"true\"\n            tools:text=\"Row Value\" />\n    </TableRow>\n\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_dropdown.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:ellipsize=\"end\"\n    android:maxLines=\"1\"\n    android:padding=\"16dp\"\n    android:textAppearance=\"?attr/textAppearanceBodyLarge\"\n    tools:text=\"Some text\" />"
  },
  {
    "path": "app/src/main/res/layout/item_episode.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_marginHorizontal=\"10dp\"\n    android:layout_marginVertical=\"5dp\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    style=\"?attr/materialCardViewOutlinedStyle\">\n    \n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n        \n        <ImageView\n            android:id=\"@+id/iv_thumbnail\"\n            android:layout_width=\"@dimen/episode_card_img_width\"\n            android:layout_height=\"@dimen/episode_card_img_height\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:contentDescription=\"@null\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_toEndOf=\"@id/iv_thumbnail\"\n            android:layout_toStartOf=\"@id/checkbox_watched\"\n            android:layout_centerVertical=\"true\"\n            android:padding=\"@dimen/card_inner_padding\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:id=\"@+id/tv_episode_title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:textAppearance=\"?attr/textAppearanceTitleLarge\"\n                android:layout_marginBottom=\"8dp\"\n                android:maxLines=\"2\"\n                android:ellipsize=\"end\"\n                tools:text=\"Some Episode Title\" />\n\n            <TextView\n                android:id=\"@+id/tv_episode_number\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:textAppearance=\"?attr/textAppearanceLabelLarge\"\n                android:maxLines=\"2\"\n                android:ellipsize=\"end\"\n                tools:text=\"Episode 1\" />\n\n        </LinearLayout>\n\n        <com.google.android.material.checkbox.MaterialCheckBox\n            android:id=\"@+id/checkbox_watched\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_centerVertical=\"true\" />\n        \n    </RelativeLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/item_facet.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"?attr/listPreferredItemHeightSmall\"\n    app:cardCornerRadius=\"0dp\"\n    style=\"@style/Widget.Kitsune.CardView.Surface\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"12dp\"\n            android:src=\"@drawable/ic_check_24\"\n            android:visibility=\"invisible\"\n            app:tint=\"?attr/colorPrimary\"\n            app:layout_constrainedHeight=\"true\"\n            app:layout_constrainedWidth=\"true\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            tools:visibility=\"visible\"\n            android:contentDescription=\"@null\" />\n\n        <TextView\n            android:id=\"@+id/facetCount\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"8dp\"\n            android:ellipsize=\"end\"\n            android:gravity=\"end\"\n            android:maxLines=\"1\"\n            android:textAppearance=\"?attr/textAppearanceLabelMedium\"\n            android:visibility=\"gone\"\n            app:layout_constrainedHeight=\"true\"\n            app:layout_constrainedWidth=\"true\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toStartOf=\"@id/icon\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            tools:text=\"@tools:sample/lorem\"\n            tools:visibility=\"visible\" />\n\n        <TextView\n            android:id=\"@+id/facetName\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"16dp\"\n            android:layout_marginEnd=\"16dp\"\n            android:ellipsize=\"end\"\n            android:maxLines=\"1\"\n            android:textAppearance=\"?attr/textAppearanceBodyLarge\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toStartOf=\"@id/facetCount\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            tools:text=\"@tools:sample/lorem/random\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n</com.google.android.material.card.MaterialCardView>\n"
  },
  {
    "path": "app/src/main/res/layout/item_library_entry.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout 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\n    <data>\n        <import type=\"io.github.drumber.kitsune.util.ui.BindingAdapter\" />\n        <import type=\"androidx.core.content.ContextCompat\" />\n        <variable\n            name=\"entry\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryWithModification\" />\n    </data>\n\n    <com.google.android.material.card.MaterialCardView\n        android:id=\"@+id/cardView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:layout_margin=\"5dp\"\n        app:cardBackgroundColor=\"?attr/colorSurfaceContainerLow\"\n        style=\"?attr/materialCardViewOutlinedStyle\">\n        \n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"@dimen/library_entry_img_height\">\n            \n            <ImageView\n                android:id=\"@+id/iv_thumbnail\"\n                android:layout_width=\"@dimen/library_entry_img_width\"\n                android:layout_height=\"@dimen/library_entry_img_height\"\n                android:layout_alignParentStart=\"true\"\n                android:layout_centerVertical=\"true\"\n                android:transitionName=\"@{@string/unique_poster_transition_name(entry.id)}\"\n                tools:src=\"@drawable/ic_insert_photo_48\"\n                android:contentDescription=\"@null\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"10dp\"\n                android:orientation=\"vertical\"\n                android:layout_alignParentEnd=\"true\"\n                android:layout_alignParentTop=\"true\"\n                android:layout_toEndOf=\"@id/iv_thumbnail\">\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/tv_title\"\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:paddingBottom=\"5dp\"\n                        android:textAppearance=\"?attr/textAppearanceTitleMedium\"\n                        android:textSize=\"18sp\"\n                        android:layout_weight=\"1\"\n                        android:ellipsize=\"end\"\n                        android:maxLines=\"3\"\n                        android:layout_marginBottom=\"8dp\"\n                        android:text=\"@{entry.media.title}\"\n                        tools:text=\"Some Title\" />\n\n                    <com.google.android.material.button.MaterialButton\n                        android:id=\"@+id/btn_rating\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"2dp\"\n                        app:icon=\"@{entry.hasRating ? @drawable/ic_star_24 : @drawable/ic_star_outline_24}\"\n                        app:iconGravity=\"end\"\n                        android:text=\"@{entry.ratingFormatted}\"\n                        app:iconPadding=\"@{entry.hasRating ? @dimen/icon_button_padding : @dimen/zero_dp}\"\n                        tools:icon=\"@drawable/ic_star_24\"\n                        tools:text=\"5\"\n                        app:tooltip=\"@{@string/hint_rating}\"\n                        app:activated=\"@{entry.hasRating}\"\n                        style=\"?attr/materialIconButtonStyle\" />\n\n                </LinearLayout>\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?attr/textAppearanceBodyMedium\"\n                    android:ellipsize=\"end\"\n                    android:maxLines=\"1\"\n                    android:text=\"@{entry.media.publishingYearText(context) + ` • ` + entry.media.subtypeFormatted}\"\n                    tools:text=\"2002 • TV\" />\n\n                <TextView\n                    android:id=\"@+id/tv_not_synced\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:paddingTop=\"4dp\"\n                    android:textAppearance=\"?attr/textAppearanceLabelMedium\"\n                    app:drawableLeftCompat=\"@drawable/ic_cloud_off_16\"\n                    android:drawablePadding=\"10dp\"\n                    android:text=\"@string/library_not_synchronized\"\n                    android:visibility=\"gone\"\n                    tools:visibility=\"visible\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_toEndOf=\"@id/iv_thumbnail\"\n                android:layout_alignParentBottom=\"true\"\n                android:layout_marginBottom=\"5dp\"\n                android:layout_alignParentEnd=\"true\"\n                android:orientation=\"horizontal\"\n                android:padding=\"10dp\">\n\n                <TextView\n                    android:id=\"@+id/tv_status\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"bottom\"\n                    android:text=\"@{entry.hasStartedWatching ? entry.progress + `/` + entry.episodeCountFormatted : @string/library_not_started}\"\n                    tools:text=\"12/24\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"0dp\"\n                    android:layout_weight=\"1\" />\n\n                <LinearLayout\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"1dp\"\n                    android:background=\"@drawable/rectangle_background\">\n\n                    <com.google.android.material.button.MaterialButton\n                        android:id=\"@+id/btn_watched_removed\"\n                        android:layout_width=\"48dp\"\n                        android:layout_height=\"36dp\"\n                        android:enabled=\"@{entry.hasStartedWatching}\"\n                        app:icon=\"@drawable/ic_remove_24\"\n                        app:iconGravity=\"textStart\"\n                        app:tooltip=\"@{@string/hint_mark_not_watched}\"\n                        app:cornerRadius=\"5dp\"\n                        style=\"@style/Widget.Kitsune.Button.IconButton.Dense\" />\n\n                    <View\n                        android:layout_width=\"1dp\"\n                        android:layout_height=\"match_parent\"\n                        android:background=\"?attr/colorButtonNormal\"\n                        android:layout_marginVertical=\"8dp\" />\n\n                    <com.google.android.material.button.MaterialButton\n                        android:id=\"@+id/btn_watched_add\"\n                        android:layout_width=\"48dp\"\n                        android:layout_height=\"36dp\"\n                        android:enabled=\"@{entry.canWatchEpisode}\"\n                        app:icon=\"@drawable/ic_add_24\"\n                        app:iconGravity=\"textStart\"\n                        app:tooltip=\"@{@string/hint_mark_watched}\"\n                        app:cornerRadius=\"5dp\"\n                        style=\"@style/Widget.Kitsune.Button.IconButton.Dense\" />\n\n                </LinearLayout>\n\n            </LinearLayout>\n\n            <com.google.android.material.progressindicator.LinearProgressIndicator\n                android:id=\"@+id/watch_progress\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_alignParentBottom=\"true\"\n                android:layout_toEndOf=\"@id/iv_thumbnail\"\n                android:layout_alignParentEnd=\"true\"\n                app:trackCornerRadius=\"0dp\"\n                app:isVisible=\"@{entry.hasEpisodesCount &amp;&amp; entry.hasStartedWatchingOrIsCurrent}\"\n                android:max=\"@{entry.episodeCount}\"\n                android:progress=\"@{entry.progress}\"\n                tools:progress=\"50\"/>\n            \n        </RelativeLayout>\n\n    </com.google.android.material.card.MaterialCardView>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_library_status_separator.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:padding=\"10dp\">\n\n    <TextView\n        android:id=\"@+id/tv_title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?attr/textAppearanceListItem\"\n        tools:text=\"Want to Watch\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_list_option.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <variable\n            name=\"icon\"\n            type=\"android.graphics.drawable.Drawable\" />\n        <variable\n            name=\"title\"\n            type=\"String\" />\n        <variable\n            name=\"subtitle\"\n            type=\"String\" />\n        <variable\n            name=\"listener\"\n            type=\"io.github.drumber.kitsune.util.ItemClickListener\" />\n    </data>\n\n    <RelativeLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:minHeight=\"56dp\"\n        android:layout_marginVertical=\"4dp\"\n        android:onClick=\"@{() -> listener.onItemClicked()}\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:background=\"@drawable/selectable_item_background\">\n\n        <ImageView\n            android:id=\"@+id/iv_icon\"\n            android:layout_width=\"20dp\"\n            android:layout_height=\"20dp\"\n            android:layout_marginStart=\"24dp\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:src=\"@{icon}\"\n            app:isVisible=\"@{icon != null}\"\n            tools:src=\"@drawable/ic_favorite_24\"\n            android:contentDescription=\"@null\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"24dp\"\n            android:layout_centerVertical=\"true\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_toEndOf=\"@id/iv_icon\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:maxLines=\"1\"\n                android:ellipsize=\"end\"\n                android:textAppearance=\"?attr/textAppearanceTitleSmall\"\n                android:text=\"@{title}\"\n                tools:text=\"Favorite\" />\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                app:isVisible=\"@{subtitle != null}\"\n                android:maxLines=\"1\"\n                android:ellipsize=\"end\"\n                android:textAppearance=\"?attr/textAppearanceBodySmall\"\n                android:text=\"@{subtitle}\"\n                tools:text=\"Some subtitle\" />\n\n        </LinearLayout>\n\n    </RelativeLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_media.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout 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\n    <data>\n        <import type=\"android.text.TextUtils\" />\n        <variable\n            name=\"data\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.media.Media\" />\n        <variable\n            name=\"overlayTagText\"\n            type=\"String\" />\n    </data>\n\n    <FrameLayout\n        android:id=\"@+id/content_wrapper\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <io.github.drumber.kitsune.ui.component.MediaItemCard\n            android:id=\"@+id/card_media\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"@dimen/media_item_height_large\"\n            android:layout_margin=\"@dimen/media_item_margin\"\n            android:layout_gravity=\"center\"\n            android:transitionName=\"@{@string/unique_poster_transition_name(data.id)}\"\n            android:clickable=\"true\"\n            android:focusable=\"true\"\n            style=\"@style/Widget.Kitsune.CardView.Surface\">\n\n            <RelativeLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <ImageView\n                    android:id=\"@+id/iv_thumbnail\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_alignParentTop=\"true\"\n                    android:layout_centerHorizontal=\"true\"\n                    tools:src=\"@drawable/ic_insert_photo_48\"\n                    android:contentDescription=\"@null\" />\n\n                <ImageView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"100dp\"\n                    android:src=\"@drawable/bottom_edge_fade\"\n                    android:layout_alignParentBottom=\"true\"\n                    android:layout_alignParentStart=\"true\"\n                    android:contentDescription=\"@null\" />\n\n                <com.google.android.material.textview.MaterialTextView\n                    android:id=\"@+id/tv_title\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_alignParentStart=\"true\"\n                    android:layout_alignParentBottom=\"true\"\n                    android:ellipsize=\"end\"\n                    android:maxLines=\"2\"\n                    android:padding=\"10dp\"\n                    android:text=\"@{data.title}\"\n                    android:textColor=\"@color/white\"\n                    tools:text=\"Title\" />\n\n                <com.google.android.material.card.MaterialCardView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_alignParentRight=\"true\"\n                    android:layout_alignParentTop=\"true\"\n                    app:cardBackgroundColor=\"@color/subtype_badge_background\"\n                    app:cardElevation=\"0dp\"\n                    app:shapeAppearance=\"@style/ShapeAppearance.Kitsune.SubtypeBadge\"\n                    app:isVisible=\"@{!TextUtils.isEmpty(overlayTagText)}\"\n                    style=\"@style/Widget.Kitsune.CardView.Surface\"\n                    tools:ignore=\"RelativeOverlap,RtlHardcoded\">\n\n                    <com.google.android.material.textview.MaterialTextView\n                        android:id=\"@+id/tv_overlay_tag\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:paddingHorizontal=\"4dp\"\n                        android:paddingVertical=\"2dp\"\n                        android:textAppearance=\"@style/TextAppearance.Kitsune.SubtypeBadge\"\n                        android:maxLines=\"1\"\n                        android:ellipsize=\"end\"\n                        android:text=\"@{overlayTagText}\"\n                        tools:text=\"TV\" />\n\n                </com.google.android.material.card.MaterialCardView>\n\n            </RelativeLayout>\n\n        </io.github.drumber.kitsune.ui.component.MediaItemCard>\n\n    </FrameLayout>\n\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_media_mapping.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"horizontal\"\n    android:padding=\"10dp\"\n    android:foreground=\"?attr/selectableItemBackground\"\n    android:gravity=\"center_vertical\"\n    android:clickable=\"true\">\n\n    <ImageView\n        android:id=\"@+id/iv_site_icon\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        android:src=\"@drawable/ic_website\"\n        android:contentDescription=\"@null\" />\n\n    <LinearLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:layout_marginHorizontal=\"20dp\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center_vertical\">\n\n        <TextView\n            android:id=\"@+id/tv_site_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?attr/textAppearanceListItem\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\"\n            tools:text=\"SomeAnimeSite\"/>\n\n        <TextView\n            android:id=\"@+id/tv_site_url\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?attr/textAppearanceListItemSecondary\"\n            android:textSize=\"12sp\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\"\n            tools:text=\"https://example.com/anime/123\"/>\n\n    </LinearLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_network_state.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:padding=\"10dp\">\n\n    <include\n        android:id=\"@+id/layout_loading\"\n        layout=\"@layout/layout_resource_loading\"/>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_profile_site_chip.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.chip.Chip 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=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:checkable=\"false\"\n    app:closeIconEnabled=\"false\"\n    style=\"@style/Widget.Material3.Chip.Input\"\n    tools:text=\"Somesite\"\n    tools:chipIcon=\"@drawable/ic_github\">\n\n</com.google.android.material.chip.Chip>"
  },
  {
    "path": "app/src/main/res/layout/item_profile_stats.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"@dimen/card_inner_padding\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <FrameLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"@dimen/profile_pie_chart_height\">\n\n            <com.github.mikephil.charting.charts.PieChart\n                android:id=\"@+id/pie_chart\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\" />\n\n            <TextView\n                android:id=\"@+id/tv_categories_no_data\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:text=\"@string/no_data_available\"\n                android:textAppearance=\"?attr/textAppearanceTitleMedium\"\n                android:visibility=\"gone\"\n                tools:visibility=\"gone\" />\n\n        </FrameLayout>\n\n        <com.google.android.material.card.MaterialCardView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            style=\"?attr/materialCardViewFilledStyle\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"@dimen/card_inner_padding\"\n                android:orientation=\"vertical\">\n\n                <TextView\n                    android:id=\"@+id/tv_time_spent\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?attr/textAppearanceTitleSmall\"\n                    tools:text=\"5.3 days spent watching anime.\" />\n\n                <TextView\n                    android:id=\"@+id/tv_time_spent_total\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    tools:text=\"5 days, 7 hours, 34 minutes in total\" />\n\n                <TextView\n                    android:id=\"@+id/tv_completed\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    tools:text=\"24 completed. More than 38% of users.\" />\n\n            </LinearLayout>\n\n        </com.google.android.material.card.MaterialCardView>\n\n    </LinearLayout>\n\n    <ProgressBar\n        android:id=\"@+id/progress_bar\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:visibility=\"gone\" />\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_single_character.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/card_character\"\n    android:layout_width=\"@dimen/media_item_width_small\"\n    android:layout_height=\"@dimen/media_item_height_small\"\n    android:layout_margin=\"@dimen/media_item_margin\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    style=\"@style/Widget.Kitsune.CardView.Surface\">\n    \n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/iv_character\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:layout_alignParentTop=\"true\"\n            android:layout_centerHorizontal=\"true\"\n            tools:src=\"@drawable/ic_insert_photo_48\"\n            android:contentDescription=\"@null\" />\n\n        <ImageView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"100dp\"\n            android:src=\"@drawable/bottom_edge_fade\"\n            android:layout_alignParentBottom=\"true\"\n            android:layout_alignParentStart=\"true\"\n            android:contentDescription=\"@null\" />\n\n        <com.google.android.material.textview.MaterialTextView\n            android:id=\"@+id/tv_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_alignParentBottom=\"true\"\n            android:ellipsize=\"end\"\n            android:maxLines=\"2\"\n            android:padding=\"10dp\"\n            android:textColor=\"@color/white\"\n            tools:text=\"Character Name\" />\n        \n    </RelativeLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/item_streamer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"@dimen/streamer_card_width\"\n    android:layout_height=\"@dimen/streamer_card_height\"\n    android:layout_marginHorizontal=\"5dp\"\n    android:focusable=\"true\"\n    android:clickable=\"true\"\n    style=\"?attr/materialCardViewFilledStyle\">\n\n    <ImageView\n        android:id=\"@+id/iv_logo\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:padding=\"10dp\"\n        android:contentDescription=\"@null\" />\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/item_theme_option.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginHorizontal=\"5dp\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.card.MaterialCardView\n        android:id=\"@+id/card_theme\"\n        android:layout_width=\"60dp\"\n        android:layout_height=\"60dp\"\n        app:cardCornerRadius=\"30dp\"\n        android:checkable=\"true\"\n        android:clickable=\"true\"\n        android:focusable=\"true\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <View\n                android:id=\"@+id/viewPrimaryColor\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"0dp\"\n                app:layout_constraintEnd_toStartOf=\"@id/viewSecondaryColor\"\n                app:layout_constraintHeight_percent=\".5\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                tools:background=\"#ff0000\" />\n\n            <View\n                android:id=\"@+id/viewSecondaryColor\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"0dp\"\n                android:layout_alignBottom=\"@id/viewSurfaceColor\"\n                app:layout_constraintStart_toEndOf=\"@id/viewPrimaryColor\"\n                app:layout_constraintBottom_toTopOf=\"@id/viewSurfaceColor\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                tools:background=\"#00ff00\" />\n\n            <View\n                android:id=\"@+id/viewSurfaceColor\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"0dp\"\n                app:layout_constraintHeight_percent=\".5\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                tools:background=\"#0000ff\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    </com.google.android.material.card.MaterialCardView>\n\n    <TextView\n        android:id=\"@+id/tv_name\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"8dp\"\n        android:maxLines=\"1\"\n        android:ellipsize=\"end\"\n        android:gravity=\"center\"\n        tools:text=\"Orange\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_resource_loading.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center\">\n\n    <TextView\n        android:id=\"@+id/tv_error\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textAlignment=\"center\"\n        android:layout_gravity=\"center_horizontal\"\n        android:text=\"@string/error_resource_loading\"/>\n\n    <TextView\n        android:id=\"@+id/tv_no_data\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:visibility=\"gone\"\n        android:text=\"@string/error_nothing_found\" />\n\n    <ProgressBar\n        android:id=\"@+id/progress_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n    <Button\n        android:id=\"@+id/btn_retry\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:text=\"@string/action_retry\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_search_provider_loading.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center\">\n\n    <TextView\n        android:id=\"@+id/tv_status\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textAlignment=\"center\"\n        android:layout_gravity=\"center_horizontal\"\n        android:text=\"@string/error_search_provider_unavailable\" />\n\n    <ProgressBar\n        android:id=\"@+id/progress_bar_search_provider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n    <Button\n        android:id=\"@+id/btn_retry_search_provider\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:text=\"@string/action_retry\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_theme_picker_preference.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"16dp\"\n    android:orientation=\"vertical\"\n    android:clipChildren=\"false\">\n\n    <TextView\n        android:id=\"@+id/tv_title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:singleLine=\"true\"\n        android:textAppearance=\"?android:attr/textAppearanceListItem\"\n        android:ellipsize=\"marquee\"\n        tools:text=\"@string/preference_app_theme\"/>\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/rv_themes\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"16dp\"\n        android:clipToPadding=\"false\"\n        android:nestedScrollingEnabled=\"false\"/>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/section_details_description.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <variable\n            name=\"data\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.media.Media\" />\n    </data>\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/title_description\"\n            android:layout_marginBottom=\"10dp\"\n            style=\"?attr/textAppearanceHeadlineSmall\" />\n\n        <at.blogc.android.views.ExpandableTextView\n            android:id=\"@+id/tv_description\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:scrollbars=\"none\"\n            tools:text=\"@tools:sample/lorem/random\"\n            android:text=\"@{data.description}\"\n            android:maxLines=\"5\"\n            android:ellipsize=\"end\"\n            app:actionButton=\"@{btnExpandCollapse}\"\n            app:animation_duration=\"200\"\n            android:textAppearance=\"?android:attr/textAppearanceSmall\" />\n\n        <com.google.android.material.button.MaterialButton\n            android:id=\"@+id/btn_expand_collapse\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/action_read_more\"\n            android:layout_gravity=\"end\"\n            style=\"@style/Widget.Material3.Button.TextButton\" />\n\n    </LinearLayout>\n\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/section_details_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <import type=\"android.text.TextUtils\" />\n        <import type=\"io.github.drumber.kitsune.data.presentation.model.media.Anime\" />\n        <import type=\"io.github.drumber.kitsune.data.presentation.model.media.production.AnimeProductionRole\" />\n\n        <variable\n            name=\"data\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.media.Media\" />\n    </data>\n\n    <TableLayout\n        android:id=\"@+id/table_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:stretchColumns=\"0,1\">\n\n        <!-- Title English -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{data.titleEn != null}\"\n            app:title=\"@{@string/data_title_english}\"\n            app:value=\"@{data.titleEn}\" />\n\n        <!-- Title Japanese -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{data.titleJaJp != null}\"\n            app:title=\"@{@string/data_title_japanese}\"\n            app:value=\"@{data.titleJaJp}\" />\n\n        <!-- Title Romaji -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{data.titleEnJp != null}\"\n            app:title=\"@{@string/data_title_japanese_romaji}\"\n            app:value=\"@{data.titleEnJp}\" />\n\n        <!-- Other titles will be dynamically added in the code here -->\n\n        <!-- Abbreviated Titles (Synonyms) -->\n        <include\n            android:id=\"@+id/synonyms_row_layout\"\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.abbreviatedTitlesFormatted)}\"\n            app:title=\"@{@string/data_abbreviated_titles}\"\n            app:value=\"@{data.abbreviatedTitlesFormatted}\" />\n\n        <!-- Subtype -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:title=\"@{@string/data_title_type}\"\n            app:value=\"@{data.subtypeFormatted}\" />\n\n        <!-- Status -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:title=\"@{@string/data_title_status}\"\n            app:value='@{data != null ? context.getString(data.statusStringRes) : \"\"}' />\n\n        <!-- Aired Date -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{TextUtils.isEmpty(data.tba) &amp;&amp; !TextUtils.isEmpty(data.airedText)}\"\n            app:title=\"@{data instanceof Anime ? @string/data_title_aired : @string/data_title_published}\"\n            app:value=\"@{data.airedText}\" />\n\n        <!-- Season -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{TextUtils.isEmpty(data.tba) &amp;&amp; !TextUtils.isEmpty(data.seasonYear) &amp;&amp; data instanceof Anime}\"\n            app:title=\"@{@string/data_title_season}\"\n            app:value='@{data != null ? context.getString(data.seasonStringRes) + ` ` + data.seasonYear : \"\"}' />\n\n        <!-- TBA (Expected Date) -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.tba)}\"\n            app:title=\"@{@string/data_title_tba}\"\n            app:value=\"@{data.tba}\" />\n\n        <!-- Age Rating -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.ageRatingText)}\"\n            app:title=\"@{@string/data_title_age_rating}\"\n            app:value=\"@{data.ageRatingText}\" />\n\n        <!-- Serialization (Manga) -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.serializationText)}\"\n            app:title=\"@{@string/data_title_serialization}\"\n            app:value=\"@{data.serializationText}\" />\n\n        <!-- Chapters (Manga) -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.chapters)}\"\n            app:title=\"@{@string/data_title_chapters}\"\n            app:value=\"@{data.chapters}\" />\n\n        <!-- Volumes (Manga) -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.volumes)}\"\n            app:title=\"@{@string/data_title_volumes}\"\n            app:value=\"@{data.volumes}\" />\n\n        <!-- Episodes (Anime) -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.episodes)}\"\n            app:title=\"@{@string/data_title_episodes}\"\n            app:value=\"@{data.episodes}\" />\n\n        <!-- Length -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.lengthText(context))}\"\n            app:title=\"@{@string/data_title_length}\"\n            app:value=\"@{data.lengthText(context)}\" />\n\n        <!-- Producers (Anime) -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.getProducer(AnimeProductionRole.Producer))}\"\n            app:title=\"@{@string/data_title_producers}\"\n            app:value=\"@{data.getProducer(AnimeProductionRole.Producer)}\" />\n\n        <!-- Licensor (Anime) -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.getProducer(AnimeProductionRole.Licensor))}\"\n            app:title=\"@{@string/data_title_licensors}\"\n            app:value=\"@{data.getProducer(AnimeProductionRole.Licensor)}\" />\n\n        <!-- Studio (Anime) -->\n        <include\n            layout=\"@layout/item_details_info_row\"\n            app:isVisible=\"@{!TextUtils.isEmpty(data.getProducer(AnimeProductionRole.Studio))}\"\n            app:title=\"@{@string/data_title_studios}\"\n            app:value=\"@{data.getProducer(AnimeProductionRole.Studio)}\" />\n\n    </TableLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/section_details_stats.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <variable\n            name=\"data\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.media.Media\" />\n    </data>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:paddingHorizontal=\"20dp\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:drawableStartCompat=\"@drawable/ic_emoji_events_24\"\n            app:drawableTint=\"@color/color_score\"\n            android:drawablePadding=\"10dp\"\n            android:gravity=\"center_vertical\"\n            tools:text=\"Kitsu Score 83.03%\"\n            app:isVisible=\"@{data.avgRatingFormatted != null}\"\n            android:text=\"@{@string/data_kitsu_score(data.avgRatingFormatted)}\" />\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            app:drawableStartCompat=\"@drawable/ic_star_24\"\n            app:drawableTint=\"@color/color_star\"\n            android:drawablePadding=\"10dp\"\n            android:gravity=\"center_vertical\"\n            tools:text=\"Rank #100\"\n            app:isVisible=\"@{data.ratingRank != null}\"\n            android:text=\"@{@string/data_rank(data.ratingRank)}\" />\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            app:drawableStartCompat=\"@drawable/ic_favorite_24\"\n            app:drawableTint=\"@color/color_heart\"\n            android:drawablePadding=\"10dp\"\n            android:gravity=\"center_vertical\"\n            tools:text=\"Popularity #100\"\n            app:isVisible=\"@{data.popularityRank != null}\"\n            android:text=\"@{@string/data_popularity(data.popularityRank)}\" />\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            app:drawableStartCompat=\"@drawable/ic_schedule_24\"\n            android:drawablePadding=\"10dp\"\n            android:gravity=\"center_vertical\"\n            tools:text=\"Currently Airing\"\n            android:text='@{data != null ? context.getString(data.statusStringRes) : \"\"}' />\n\n    </LinearLayout>\n\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/section_details_trailer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <variable\n            name=\"videoUrl\"\n            type=\"String\" />\n        <variable\n            name=\"coverUrl\"\n            type=\"String\" />\n    </data>\n\n    <com.google.android.material.card.MaterialCardView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        app:openOnClick=\"@{videoUrl}\"\n        style=\"@style/Widget.Kitsune.CardView.Surface\">\n\n        <FrameLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <ImageView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"@dimen/details_trailer_height\"\n                android:src=\"@drawable/ic_insert_photo_48\"\n                app:imageUrl=\"@{coverUrl}\"\n                android:contentDescription=\"@null\" />\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"@dimen/details_trailer_height\"\n                android:layout_gravity=\"bottom\"\n                android:background=\"@drawable/radial_edge_fade\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:gravity=\"center\"\n                android:padding=\"@dimen/card_inner_padding\"\n                android:orientation=\"horizontal\">\n\n                <ImageView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"70dp\"\n                    android:layout_weight=\"2\"\n                    android:layout_marginEnd=\"20dp\"\n                    android:scaleType=\"fitCenter\"\n                    android:src=\"@drawable/ic_youtube\"\n                    app:tint=\"@color/white\"\n                    android:contentDescription=\"@null\" />\n\n                <TextView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"1\"\n                    app:autoSizeTextType=\"uniform\"\n                    android:textColor=\"@color/white\"\n                    android:maxLines=\"1\"\n                    android:ellipsize=\"end\"\n                    android:text=\"@string/title_play_trailer\" />\n\n            </LinearLayout>\n\n        </FrameLayout>\n\n    </com.google.android.material.card.MaterialCardView>\n\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/section_main_explore.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <RelativeLayout\n        android:id=\"@+id/header\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?attr/selectableItemBackground\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:padding=\"10dp\">\n\n        <TextView\n            android:id=\"@+id/tv_title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_alignParentTop=\"true\"\n            android:layout_toStartOf=\"@id/iv_arrow\"\n            android:ellipsize=\"end\"\n            android:maxLines=\"1\"\n            tools:text=\"Trending This Week\"\n            android:textAppearance=\"?attr/textAppearanceTitleMedium\" />\n\n        <ImageView\n            android:id=\"@+id/iv_arrow\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentTop=\"true\"\n            android:layout_alignParentEnd=\"true\"\n            android:src=\"@drawable/ic_arrow_forward_24\"\n            android:contentDescription=\"@null\" />\n\n    </RelativeLayout>\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <io.github.drumber.kitsune.ui.component.NestedScrollableHost\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <io.github.drumber.kitsune.ui.component.UniqueStateRecyclerView\n                android:id=\"@+id/rv_media\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:paddingHorizontal=\"10dp\"\n                android:clipToPadding=\"false\"\n                android:orientation=\"horizontal\"\n                android:nestedScrollingEnabled=\"false\"\n                app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n                android:transitionGroup=\"true\"\n                tools:listitem=\"@layout/item_media\" />\n\n        </io.github.drumber.kitsune.ui.component.NestedScrollableHost>\n\n        <include\n            android:id=\"@+id/layout_loading\"\n            layout=\"@layout/layout_resource_loading\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            tools:layout_height=\"@dimen/media_item_height_large\"\n            android:layout_gravity=\"center\"\n            android:visibility=\"gone\"\n            tools:visibility=\"visible\" />\n\n    </FrameLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/sheet_character_details.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <import type=\"android.text.TextUtils\" />\n        <variable\n            name=\"character\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.character.Character\" />\n    </data>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:padding=\"10dp\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"@{character.name}\"\n                style=\"@style/Widget.Kitsune.TextView.BottomSheetTitle\"\n                tools:text=\"Character Name\"/>\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/btn_favorite\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"10dp\"\n                app:icon=\"@drawable/ic_favorite_border_24\"\n                app:tooltip=\"@{@string/action_add_to_favorites}\"\n                app:iconTint=\"?attr/colorControlNormal\"\n                style=\"?attr/materialIconButtonStyle\" />\n\n        </LinearLayout>\n\n        <androidx.core.widget.NestedScrollView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:orientation=\"vertical\"\n                android:padding=\"10dp\">\n\n                <RelativeLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\">\n\n                    <ImageView\n                        android:id=\"@+id/iv_character\"\n                        android:layout_width=\"@dimen/media_item_width_small\"\n                        android:layout_height=\"wrap_content\"\n                        android:scaleType=\"fitCenter\"\n                        android:adjustViewBounds=\"true\"\n                        android:src=\"@drawable/ic_insert_photo_48\"\n                        android:transitionName=\"@string/character_picture_transition_name\"\n                        android:layout_alignParentStart=\"true\"\n                        android:layout_alignParentTop=\"true\"/>\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:orientation=\"vertical\"\n                        android:layout_marginStart=\"20dp\"\n                        android:layout_toEndOf=\"@id/iv_character\"\n                        android:layout_alignParentTop=\"true\"\n                        android:layout_alignParentEnd=\"true\">\n\n                        <TableLayout\n                            android:id=\"@+id/table_names\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:stretchColumns=\"0,1\">\n\n                            <!-- Names will be dynamically added in code -->\n\n                            <include\n                                layout=\"@layout/item_details_info_row\"\n                                app:isVisible=\"@{character.otherNames != null &amp;&amp; !character.otherNames.empty}\"\n                                app:title=\"@{@string/data_other_names}\"\n                                app:value='@{character.otherNames != null ? TextUtils.join(\", \", character.otherNames) : \"\"}' />\n\n                        </TableLayout>\n\n                    </LinearLayout>\n\n                </RelativeLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\"\n                    android:layout_marginTop=\"20dp\"\n                    app:isVisible=\"@{!TextUtils.isEmpty(character.description)}\">\n\n                    <at.blogc.android.views.ExpandableTextView\n                        android:id=\"@+id/tv_description\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:scrollbars=\"none\"\n                        android:text=\"@{character.description}\"\n                        android:maxLines=\"5\"\n                        android:ellipsize=\"end\"\n                        app:actionButton=\"@{btnExpandCollapse}\"\n                        app:animation_duration=\"200\"\n                        android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                        tools:text=\"@tools:sample/lorem/random\" />\n\n                    <com.google.android.material.button.MaterialButton\n                        android:id=\"@+id/btn_expand_collapse\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:text=\"@string/action_read_more\"\n                        android:layout_gravity=\"end\"\n                        style=\"@style/Widget.Material3.Button.TextButton\" />\n\n                </LinearLayout>\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"10dp\"\n                    android:layout_marginBottom=\"10dp\"\n                    android:text=\"@string/title_character_appearances\"\n                    android:textAppearance=\"?attr/textAppearanceHeadlineSmall\" />\n\n                <androidx.recyclerview.widget.RecyclerView\n                    android:id=\"@+id/rv_media_characters\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:nestedScrollingEnabled=\"false\"\n                    android:clipToPadding=\"false\"\n                    android:orientation=\"horizontal\"\n                    android:paddingVertical=\"5dp\"\n                    android:requiresFadingEdge=\"horizontal\"\n                    app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\" />\n\n                <FrameLayout\n                    android:id=\"@+id/loading_wrapper\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"@dimen/media_item_height_small\">\n\n                    <ProgressBar\n                        android:id=\"@+id/progress_bar\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"/>\n\n                    <TextView\n                        android:id=\"@+id/tv_no_data\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:text=\"@string/no_data_available\" />\n\n                </FrameLayout>\n\n                <Button\n                    android:id=\"@+id/btn_open_on_mal\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"20dp\"\n                    android:layout_gravity=\"end\"\n                    android:text=\"@string/action_open_on_mal\"\n                    app:icon=\"@drawable/ic_open_in_new_24\"\n                    style=\"@style/Widget.Material3.Button.TextButton.Icon\" />\n\n            </LinearLayout>\n\n        </androidx.core.widget.NestedScrollView>\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/sheet_edit_profile_link.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <variable\n            name=\"isCreatingNew\"\n            type=\"boolean\" />\n    </data>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"18dp\"\n        android:orientation=\"vertical\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"20dp\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"@{isCreatingNew ? @string/action_add_profile_link : @string/action_edit_profile_link}\"\n                style=\"@style/Widget.Kitsune.TextView.BottomSheetTitle\"\n                tools:text=\"@string/action_edit_profile_link\" />\n\n            <Button\n                android:id=\"@+id/btn_delete\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:tooltipText=\"@string/action_delete_profile_link\"\n                app:icon=\"@drawable/ic_delete_24\"\n                app:isVisible=\"@{!isCreatingNew}\"\n                style=\"@style/Widget.Material3.Button.IconButton\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:gravity=\"center_vertical\"\n            android:layout_marginBottom=\"10dp\">\n\n            <ImageView\n                android:id=\"@+id/iv_logo\"\n                android:layout_width=\"25dp\"\n                android:layout_height=\"25dp\"\n                android:layout_marginEnd=\"10dp\"\n                tools:src=\"@drawable/ic_github\"\n                android:contentDescription=\"@null\" />\n\n            <TextView\n                android:id=\"@+id/tv_site_name\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                tools:text=\"GitHub\" />\n\n        </LinearLayout>\n\n        <com.google.android.material.textfield.TextInputLayout\n            android:id=\"@+id/field_url\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:hint=\"@string/profile_link_url\">\n\n            <com.google.android.material.textfield.TextInputEditText\n                android:id=\"@+id/et_url\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:inputType=\"text\"\n                android:maxLines=\"1\" />\n\n        </com.google.android.material.textfield.TextInputLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"20dp\"\n            android:orientation=\"horizontal\">\n\n            <Button\n                android:id=\"@+id/btn_cancel\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"@string/action_cancel\"\n                style=\"?attr/buttonBarNeutralButtonStyle\" />\n\n            <Button\n                android:id=\"@+id/btn_confirm\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"@{isCreatingNew ? @string/action_add : @string/action_update}\"\n                style=\"?attr/buttonBarPositiveButtonStyle\"\n                tools:text=\"@string/action_update\" />\n\n        </LinearLayout>\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/sheet_library_rating.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <import type=\"io.github.drumber.kitsune.util.ui.BindingAdapter\" />\n        <variable\n            name=\"title\"\n            type=\"String\" />\n    </data>\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\"\n            android:padding=\"10dp\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                android:layout_marginTop=\"10dp\"\n                android:layout_marginBottom=\"20dp\">\n\n                <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"@{title}\"\n                    tools:text=\"Some Title\"\n                    style=\"@style/Widget.Kitsune.TextView.BottomSheetTitle\" />\n\n                <com.google.android.material.button.MaterialButton\n                    android:id=\"@+id/btn_remove_rating\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginStart=\"10dp\"\n                    app:icon=\"@drawable/ic_delete_24\"\n                    app:tooltip=\"@{@string/action_remove_rating}\"\n                    app:iconTint=\"?attr/colorControlNormal\"\n                    style=\"?attr/materialIconButtonStyle\" />\n\n            </LinearLayout>\n\n            <me.zhanghai.android.materialratingbar.MaterialRatingBar\n                android:id=\"@+id/rating_bar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                tools:progress=\"50\"\n                style=\"@style/Widget.MaterialRatingBar.RatingBar\" />\n\n            <TextView\n                android:id=\"@+id/tv_rating\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:layout_marginTop=\"10dp\"\n                android:textAppearance=\"?attr/textAppearanceDisplaySmall\"\n                tools:text=\"2.5 / 5.0\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:weightSum=\"2\"\n                android:orientation=\"horizontal\">\n\n                <com.google.android.material.button.MaterialButton\n                    android:id=\"@+id/btn_cancel\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginEnd=\"5dp\"\n                    android:text=\"@android:string/cancel\"\n                    style=\"@style/Widget.Material3.Button.TextButton.Dialog\" />\n\n                <com.google.android.material.button.MaterialButton\n                    android:id=\"@+id/btn_rate\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginStart=\"5dp\"\n                    android:text=\"@string/action_rate\"\n                    style=\"@style/Widget.Material3.Button.TextButton.Dialog\" />\n\n            </LinearLayout>\n\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/sheet_manage_library.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout 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\n    <data>\n        <import type=\"io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\" />\n        <variable\n            name=\"title\"\n            type=\"String\" />\n        <variable\n            name=\"instance\"\n            type=\"io.github.drumber.kitsune.ui.details.ManageLibraryBottomSheet\" />\n    </data>\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\"\n            android:padding=\"10dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:layout_marginBottom=\"10dp\"\n                android:text=\"@{title}\"\n                tools:text=\"Some Title\"\n                style=\"@style/Widget.Kitsune.TextView.BottomSheetTitle\" />\n\n            <include\n                layout=\"@layout/item_list_option\"\n                app:listener=\"@{() -> instance.onStatusClicked(LibraryStatus.Current)}\"\n                app:title=\"@{instance.isAnime() ? @string/library_status_watching : @string/library_status_reading}\"\n                app:icon=\"@{@drawable/ic_incomplete_circle_24}\" />\n\n            <include\n                layout=\"@layout/item_list_option\"\n                app:listener=\"@{() -> instance.onStatusClicked(LibraryStatus.Planned)}\"\n                app:title=\"@{instance.isAnime() ? @string/library_status_planned : @string/library_status_planned_manga}\"\n                app:icon=\"@{@drawable/ic_bookmark_added_24}\" />\n\n            <include\n                layout=\"@layout/item_list_option\"\n                app:listener=\"@{() -> instance.onStatusClicked(LibraryStatus.Completed)}\"\n                app:title=\"@{@string/library_status_completed}\"\n                app:icon=\"@{@drawable/ic_done_24}\" />\n\n            <include\n                layout=\"@layout/item_list_option\"\n                app:listener=\"@{() -> instance.onStatusClicked(LibraryStatus.OnHold)}\"\n                app:title=\"@{@string/library_status_on_hold}\"\n                app:icon=\"@{@drawable/ic_watch_later_24}\" />\n\n            <include\n                layout=\"@layout/item_list_option\"\n                app:listener=\"@{() -> instance.onStatusClicked(LibraryStatus.Dropped)}\"\n                app:title=\"@{@string/library_status_dropped}\"\n                app:icon=\"@{@drawable/ic_cancel_presentation_24}\"/>\n\n            <include\n                layout=\"@layout/item_list_option\"\n                app:isVisible=\"@{instance.existsInLibrary()}\"\n                app:listener=\"@{() -> instance.onRemoveClicked()}\"\n                app:title=\"@{@string/library_action_remove}\"\n                app:icon=\"@{@drawable/ic_delete_24}\"/>\n\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/sheet_media_mappings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_marginTop=\"10dp\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginHorizontal=\"10dp\"\n        android:layout_marginTop=\"20dp\"\n        android:text=\"@string/action_open_external\"\n        style=\"@style/Widget.Kitsune.TextView.BottomSheetTitle\" />\n\n    <ProgressBar\n        android:id=\"@+id/progress_bar_media_mappings\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_margin=\"20dp\"\n        android:visibility=\"gone\"\n        tools:visibility=\"visible\" />\n\n    <TextView\n        android:id=\"@+id/tv_error_media_mappings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"20dp\"\n        android:text=\"@string/error_mapping_loading\"\n        android:layout_marginVertical=\"20dp\"\n        android:gravity=\"center\"\n        android:visibility=\"gone\"\n        tools:visibility=\"visible\" />\n\n    <ListView\n        android:id=\"@+id/list_media_mappings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:nestedScrollingEnabled=\"true\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/sheet_media_unit_details.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n        <variable\n            name=\"mediaUnit\"\n            type=\"io.github.drumber.kitsune.data.presentation.model.media.unit.MediaUnit\" />\n    </data>\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\">\n            \n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n\n                <ImageView\n                    android:id=\"@+id/iv_thumbnail\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"200dp\"\n                    android:contentDescription=\"@null\"\n                    tools:src=\"@drawable/ic_insert_photo_48\" />\n\n                <View\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"100dp\"\n                    android:layout_gravity=\"bottom\"\n                    android:background=\"@drawable/bottom_edge_fade\" />\n\n                <TextView\n                    android:id=\"@+id/tv_title\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"bottom\"\n                    android:layout_margin=\"10dp\"\n                    android:textAppearance=\"?attr/textAppearanceHeadlineSmall\"\n                    android:maxLines=\"3\"\n                    android:ellipsize=\"end\"\n                    android:textColor=\"@color/white\"\n                    android:text=\"@{mediaUnit.title(context)}\"\n                    tools:text=\"Some Title\" />\n\n            </FrameLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:layout_marginBottom=\"10dp\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:gravity=\"center\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginHorizontal=\"10dp\"\n                    android:text=\"@{mediaUnit.hasValidTitle ? mediaUnit.numberText(context) : null}\"\n                    tools:text=\"Episode 2\" />\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:gravity=\"center\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginHorizontal=\"10dp\"\n                    android:text=\"@{mediaUnit.formatDate()}\"\n                    tools:text=\"21/1/1\" />\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:gravity=\"center\"\n                    android:layout_weight=\"1\"\n                    android:layout_marginHorizontal=\"10dp\"\n                    android:text=\"@{mediaUnit.length(context)}\"\n                    tools:text=\"23 minutes\" />\n\n            </LinearLayout>\n\n            <TextView\n                android:id=\"@+id/tv_description\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"10dp\"\n                app:isVisible=\"@{mediaUnit.description != null}\"\n                android:text=\"@{mediaUnit.description}\"\n                tools:text=\"Some description text.\" />\n\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/sheet_select_profile_link_site.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_marginTop=\"10dp\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"10dp\"\n        android:text=\"@string/action_select_profile_link_site\"\n        style=\"@style/Widget.Kitsune.TextView.BottomSheetTitle\" />\n\n    <ProgressBar\n        android:id=\"@+id/progress_bar_profile_link_sites\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_margin=\"20dp\"\n        android:visibility=\"gone\"\n        tools:visibility=\"visible\" />\n\n    <androidx.core.widget.NestedScrollView\n        android:id=\"@+id/nsv_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <LinearLayout\n            android:id=\"@+id/layout_list_parent\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"10dp\"\n            android:orientation=\"vertical\">\n\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout-w600dp/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"io.github.drumber.kitsune.ui.main.MainActivity\">\n\n    <com.google.android.material.navigationrail.NavigationRailView\n        android:id=\"@+id/navigation_rail\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_alignParentStart=\"true\"\n        app:labelVisibilityMode=\"labeled\"\n        app:menuGravity=\"center\"\n        app:menu=\"@menu/main_nav_menu\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/nav_host_fragment\"\n        android:name=\"androidx.navigation.fragment.NavHostFragment\"\n        app:defaultNavHost=\"true\"\n        app:navGraph=\"@navigation/main_nav_graph\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_toEndOf=\"@id/navigation_rail\" />\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/menu/category_dialog_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/unselect_all\"\n        android:title=\"@string/action_unselect_all\"\n        android:icon=\"@drawable/ic_restore_24\"\n        app:showAsAction=\"ifRoom\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/details_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_favorite\"\n        android:title=\"@string/action_add_to_favorites\"\n        android:icon=\"@drawable/ic_favorite_border_24\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/menu_share_media\"\n        android:title=\"@string/action_share\"\n        android:icon=\"@drawable/ic_share_24\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/menu_open_external\"\n        android:title=\"@string/action_open_external_short\"\n        android:icon=\"@drawable/ic_open_in_new_24\"\n        app:showAsAction=\"never\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/filter_facet_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_reset_filter\"\n        android:title=\"@string/action_reset_filter\"\n        android:icon=\"@drawable/ic_restore_24\"\n        app:showAsAction=\"ifRoom\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/library_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_synchronize\"\n        android:title=\"@string/action_synchronize\"\n        android:icon=\"@drawable/ic_sync_24\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/menu_search\"\n        android:title=\"@string/hint_search\"\n        android:icon=\"@drawable/ic_search_24\"\n        app:showAsAction=\"always|collapseActionView\"\n        app:actionViewClass=\"androidx.appcompat.widget.SearchView\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/logs_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_share_app_logs\"\n        android:title=\"@string/action_share_app_logs\"\n        app:showAsAction=\"never\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/main_nav_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item\n        android:id=\"@+id/main_fragment\"\n        android:icon=\"@drawable/selector_home\"\n        android:title=\"@string/nav_home\"/>\n\n    <item\n        android:id=\"@+id/search_fragment\"\n        android:icon=\"@drawable/selector_search\"\n        android:title=\"@string/nav_search\" />\n\n    <item\n        android:id=\"@+id/library_fragment\"\n        android:icon=\"@drawable/selector_library\"\n        android:title=\"@string/nav_library\" />\n\n    <item\n        android:id=\"@+id/profile_fragment\"\n        android:icon=\"@drawable/selector_profile\"\n        android:title=\"@string/nav_profile\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/profile_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_edit_profile\"\n        android:title=\"@string/action_edit_profile\"\n        android:icon=\"@drawable/ic_edit_24\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/menu_settings\"\n        android:title=\"@string/nav_settings\"\n        android:icon=\"@drawable/ic_settings_24\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/menu_share_profile_url\"\n        android:title=\"@string/action_share_profile_url\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/menu_log_out\"\n        android:title=\"@string/action_log_out\"\n        app:showAsAction=\"never\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/webview_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_open_in_browser\"\n        android:title=\"@string/action_open_in_browser\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/menu_copy_url\"\n        android:title=\"@string/action_copy_url\"\n        app:showAsAction=\"never\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/navigation/main_nav_graph.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/main_nav_graph\"\n    app:startDestination=\"@id/main_fragment\">\n\n    <fragment\n        android:id=\"@+id/main_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.main.MainFragment\"\n        android:label=\"@string/nav_home\"\n        tools:layout=\"@layout/fragment_main\" >\n        <action\n            android:id=\"@+id/action_main_fragment_to_detailsFragment\"\n            app:destination=\"@id/details_fragment\"\n            app:launchSingleTop=\"true\" />\n        <action\n            android:id=\"@+id/action_main_fragment_to_mediaListFragment\"\n            app:destination=\"@id/media_list_fragment\" />\n    </fragment>\n\n    <fragment\n        android:id=\"@+id/search_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.search.SearchFragment\"\n        android:label=\"@string/nav_search\"\n        tools:layout=\"@layout/fragment_search\" >\n        <action\n            android:id=\"@+id/action_search_fragment_to_details_fragment\"\n            app:destination=\"@id/details_fragment\"\n            app:launchSingleTop=\"true\" />\n        <deepLink\n            android:id=\"@+id/deepLink_search\"\n            app:uri=\"kitsune://search\" />\n        <action\n            android:id=\"@+id/action_search_fragment_to_facet_fragment\"\n            app:destination=\"@id/facet_fragment\"\n            app:enterAnim=\"@animator/scale_enter_anim\"\n            app:exitAnim=\"@animator/scale_exit_anim\"\n            app:popEnterAnim=\"@animator/scale_pop_enter_anim\"\n            app:popExitAnim=\"@animator/scale_pop_exit_anim\" />\n    </fragment>\n\n    <fragment\n        android:id=\"@+id/profile_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.profile.ProfileFragment\"\n        android:label=\"@string/nav_profile\"\n        tools:layout=\"@layout/fragment_profile\" >\n        <action\n            android:id=\"@+id/action_profile_fragment_to_details_fragment\"\n            app:destination=\"@id/details_fragment\"\n            app:launchSingleTop=\"true\" />\n        <action\n            android:id=\"@+id/action_profile_fragment_to_settings_nav_graph\"\n            app:destination=\"@id/settings_nav_graph\"\n            app:enterAnim=\"@animator/scale_enter_anim\"\n            app:exitAnim=\"@animator/scale_exit_anim\"\n            app:popEnterAnim=\"@animator/scale_pop_enter_anim\"\n            app:popExitAnim=\"@animator/scale_pop_exit_anim\" />\n        <action\n            android:id=\"@+id/action_profile_fragment_to_editProfileFragment\"\n            app:destination=\"@id/editProfileFragment\" />\n        <action\n            android:id=\"@+id/action_profile_fragment_to_characterDetailsBottomSheet\"\n            app:destination=\"@id/characterDetailsBottomSheet\" />\n    </fragment>\n    <fragment\n        android:id=\"@+id/details_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.details.DetailsFragment\"\n        android:label=\"DetailsFragment\">\n        <argument\n            android:name=\"media\"\n            android:defaultValue=\"@null\"\n            app:argType=\"io.github.drumber.kitsune.data.presentation.dto.MediaDto\"\n            app:nullable=\"true\" />\n        <action\n            android:id=\"@+id/action_details_fragment_to_media_list_fragment\"\n            app:destination=\"@id/media_list_fragment\" />\n        <action\n            android:id=\"@+id/action_details_fragment_self\"\n            app:destination=\"@id/details_fragment\"\n            app:launchSingleTop=\"false\" />\n        <action\n            android:id=\"@+id/action_details_fragment_to_episodes_fragment\"\n            app:destination=\"@id/episodes_fragment\"\n            app:enterAnim=\"@animator/scale_enter_anim\"\n            app:exitAnim=\"@animator/scale_exit_anim\"\n            app:popEnterAnim=\"@animator/scale_pop_enter_anim\"\n            app:popExitAnim=\"@animator/scale_pop_exit_anim\" />\n        <action\n            android:id=\"@+id/action_details_fragment_to_characters_fragment\"\n            app:destination=\"@id/characters_fragment\"\n            app:enterAnim=\"@animator/scale_enter_anim\"\n            app:exitAnim=\"@animator/scale_exit_anim\"\n            app:popEnterAnim=\"@animator/scale_pop_enter_anim\"\n            app:popExitAnim=\"@animator/scale_pop_exit_anim\" />\n        <action\n            android:id=\"@+id/action_details_fragment_to_libraryEditEntryFragment\"\n            app:destination=\"@id/libraryEditEntryFragment\" />\n        <deepLink\n            android:id=\"@+id/deepLink\"\n            app:uri=\"kitsu.app/{type}/{slug}\" />\n        <argument\n            android:name=\"type\"\n            android:defaultValue=\"@null\"\n            app:argType=\"string\"\n            app:nullable=\"true\" />\n        <argument\n            android:name=\"slug\"\n            android:defaultValue=\"@null\"\n            app:argType=\"string\"\n            app:nullable=\"true\" />\n    </fragment>\n    <fragment\n        android:id=\"@+id/media_list_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.medialist.MediaListFragment\"\n        android:label=\"fragment_media_list\"\n        tools:layout=\"@layout/fragment_media_list\">\n        <action\n            android:id=\"@+id/action_mediaListFragment_to_details_fragment\"\n            app:destination=\"@id/details_fragment\"\n            app:launchSingleTop=\"true\" />\n        <argument\n            android:name=\"mediaSelector\"\n            app:argType=\"io.github.drumber.kitsune.data.presentation.model.media.MediaSelector\" />\n        <argument\n            android:name=\"title\"\n            app:argType=\"string\" />\n    </fragment>\n    <fragment\n        android:id=\"@+id/library_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.library.LibraryFragment\"\n        android:label=\"@string/nav_library\"\n        tools:layout=\"@layout/fragment_library\">\n        <action\n            android:id=\"@+id/action_library_fragment_to_details_fragment\"\n            app:destination=\"@id/details_fragment\"\n            app:launchSingleTop=\"true\" />\n        <deepLink\n            android:id=\"@+id/deepLink_library\"\n            app:uri=\"kitsune://library\" />\n        <action\n            android:id=\"@+id/action_library_fragment_to_libraryEditEntryFragment\"\n            app:destination=\"@id/libraryEditEntryFragment\" />\n        <action\n            android:id=\"@+id/action_library_fragment_to_ratingBottomSheet\"\n            app:destination=\"@id/ratingBottomSheet\" />\n    </fragment>\n    <fragment\n        android:id=\"@+id/episodes_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.details.episodes.EpisodesFragment\"\n        android:label=\"EpisodesFragment\" >\n        <argument\n            android:name=\"media\"\n            app:argType=\"io.github.drumber.kitsune.data.presentation.dto.MediaDto\" />\n    </fragment>\n    <fragment\n        android:id=\"@+id/characters_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.details.characters.CharactersFragment\"\n        android:label=\"CharactersFragment\" >\n        <argument\n            android:name=\"mediaId\"\n            app:argType=\"string\" />\n        <argument\n            android:name=\"isAnime\"\n            app:argType=\"boolean\" />\n        <action\n            android:id=\"@+id/action_characters_fragment_to_characterDetailsBottomSheet\"\n            app:destination=\"@id/characterDetailsBottomSheet\" />\n    </fragment>\n    <activity\n        android:id=\"@+id/photo_view_activity\"\n        android:name=\"io.github.drumber.kitsune.ui.photoview.PhotoViewActivity\"\n        android:label=\"activity_photo_view\"\n        tools:layout=\"@layout/activity_photo_view\" >\n        <argument\n            android:name=\"imageUrl\"\n            app:argType=\"string\" />\n        <argument\n            android:name=\"title\"\n            app:argType=\"string\"\n            app:nullable=\"true\" />\n        <argument\n            android:name=\"thumbnailUrl\"\n            app:argType=\"string\"\n            app:nullable=\"true\" />\n        <argument\n            android:name=\"transitionName\"\n            app:argType=\"string\"\n            app:nullable=\"true\"\n            android:defaultValue=\"@null\" />\n    </activity>\n    <fragment\n        android:id=\"@+id/facet_fragment\"\n        android:name=\"io.github.drumber.kitsune.ui.search.filter.FacetFragment\"\n        android:label=\"FacetFragment\" />\n    <dialog\n        android:id=\"@+id/libraryEditEntryFragment\"\n        android:name=\"io.github.drumber.kitsune.ui.library.editentry.LibraryEditEntryFragment\"\n        android:label=\"LibraryEditEntryFragment\" >\n        <argument\n            android:name=\"libraryEntryId\"\n            app:argType=\"string\" />\n        <action\n            android:id=\"@+id/action_libraryEditEntryFragment_to_ratingBottomSheet\"\n            app:destination=\"@id/ratingBottomSheet\" />\n        <argument\n            android:name=\"entryUpdatedResultKey\"\n            app:argType=\"string\"\n            app:nullable=\"true\"\n            android:defaultValue=\"@null\" />\n    </dialog>\n    <dialog\n        android:id=\"@+id/ratingBottomSheet\"\n        android:name=\"io.github.drumber.kitsune.ui.library.RatingBottomSheet\"\n        android:label=\"RatingBottomSheet\" >\n        <argument\n            android:name=\"title\"\n            app:argType=\"string\" />\n        <argument\n            android:name=\"ratingTwenty\"\n            app:argType=\"integer\" />\n        <argument\n            android:name=\"ratingResultKey\"\n            app:argType=\"string\" />\n        <argument android:name=\"removeResultKey\" />\n        <argument\n            android:name=\"ratingSystem\"\n            app:argType=\"io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\" />\n    </dialog>\n    <include app:graph=\"@navigation/settings_nav_graph\" />\n    <dialog\n        android:id=\"@+id/editProfileFragment\"\n        android:name=\"io.github.drumber.kitsune.ui.profile.editprofile.EditProfileFragment\"\n        android:label=\"EditProfileFragment\" />\n    <dialog\n        android:id=\"@+id/characterDetailsBottomSheet\"\n        android:name=\"io.github.drumber.kitsune.ui.details.characters.CharacterDetailsBottomSheet\"\n        android:label=\"CharacterDetailsBottomSheet\" >\n        <argument\n            android:name=\"character\"\n            app:argType=\"io.github.drumber.kitsune.data.presentation.dto.CharacterDto\" />\n        <action\n            android:id=\"@+id/action_characterDetailsBottomSheet_to_details_fragment\"\n            app:destination=\"@id/details_fragment\" />\n    </dialog>\n    <action\n        android:id=\"@+id/action_global_photo_view_activity\"\n        app:destination=\"@id/photo_view_activity\" /><action android:id=\"@+id/action_global_details_fragment\" app:destination=\"@id/details_fragment\"/>\n    <activity\n        android:id=\"@+id/onboardingActivity\"\n        android:name=\"io.github.drumber.kitsune.ui.onboarding.OnboardingActivity\"\n        android:label=\"OnboardingActivity\" />\n    <action\n        android:id=\"@+id/action_global_onboardingActivity\"\n        app:destination=\"@id/onboardingActivity\"\n        app:launchSingleTop=\"true\" />\n    <fragment\n        android:id=\"@+id/webViewFragment\"\n        android:name=\"io.github.drumber.kitsune.ui.webview.WebViewFragment\"\n        android:label=\"fragment_web_view\"\n        tools:layout=\"@layout/fragment_web_view\" >\n        <argument\n            android:name=\"url\"\n            app:argType=\"string\" />\n    </fragment>\n    <action android:id=\"@+id/action_global_webViewFragment\" app:destination=\"@id/webViewFragment\" />\n</navigation>"
  },
  {
    "path": "app/src/main/res/navigation/settings_nav_graph.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/settings_nav_graph\"\n    app:startDestination=\"@id/settingsFragment\">\n\n    <fragment\n        android:id=\"@+id/settingsFragment\"\n        android:name=\"io.github.drumber.kitsune.ui.settings.SettingsFragment\"\n        android:label=\"SettingsFragment\">\n        <action\n            android:id=\"@+id/action_settingsFragment_to_appLogsFragment\"\n            app:destination=\"@id/appLogsFragment\" />\n        <action\n            android:id=\"@+id/action_settingsFragment_to_librariesFragment\"\n            app:destination=\"@id/osLibrariesFragment\" />\n        <action\n            android:id=\"@+id/action_settingsFragment_to_appearanceFragment\"\n            app:destination=\"@id/appearanceFragment\" />\n    </fragment>\n    <fragment\n        android:id=\"@+id/appLogsFragment\"\n        android:name=\"io.github.drumber.kitsune.ui.settings.AppLogsFragment\"\n        android:label=\"fragment_app_logs\"\n        tools:layout=\"@layout/fragment_app_logs\" />\n    <fragment\n        android:id=\"@+id/osLibrariesFragment\"\n        android:name=\"io.github.drumber.kitsune.ui.settings.OSLibrariesFragment\"\n        android:label=\"OSLibrariesFragment\" />\n    <fragment\n        android:id=\"@+id/appearanceFragment\"\n        android:name=\"io.github.drumber.kitsune.ui.settings.AppearanceFragment\"\n        android:label=\"AppearanceFragment\" />\n</navigation>"
  },
  {
    "path": "app/src/main/res/resources.properties",
    "content": "unqualifiedResLocale=en-US\n"
  },
  {
    "path": "app/src/main/res/values/MdcThemeAdapter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"MdcThemeAdapter\">\n        <attr name=\"isMaterial3Theme\"/>\n\n        <attr name=\"colorPrimary\"/>\n        <attr name=\"colorOnPrimary\"/>\n        <attr name=\"colorPrimaryInverse\"/>\n        <attr name=\"colorPrimaryContainer\"/>\n        <attr name=\"colorOnPrimaryContainer\"/>\n        <attr name=\"colorSecondary\"/>\n        <attr name=\"colorOnSecondary\"/>\n        <attr name=\"colorSecondaryContainer\"/>\n        <attr name=\"colorOnSecondaryContainer\"/>\n        <attr name=\"colorTertiary\"/>\n        <attr name=\"colorOnTertiary\"/>\n        <attr name=\"colorTertiaryContainer\"/>\n        <attr name=\"colorOnTertiaryContainer\"/>\n        <attr name=\"android:colorBackground\"/>\n        <attr name=\"colorOnBackground\"/>\n        <attr name=\"colorSurface\"/>\n        <attr name=\"colorOnSurface\"/>\n        <attr name=\"colorSurfaceVariant\"/>\n        <attr name=\"colorOnSurfaceVariant\"/>\n        <attr name=\"elevationOverlayColor\"/>\n        <attr name=\"colorSurfaceInverse\"/>\n        <attr name=\"colorOnSurfaceInverse\"/>\n        <attr name=\"colorSurfaceBright\"/>\n        <attr name=\"colorSurfaceContainer\"/>\n        <attr name=\"colorSurfaceContainerHigh\"/>\n        <attr name=\"colorSurfaceContainerHighest\"/>\n        <attr name=\"colorSurfaceContainerLow\"/>\n        <attr name=\"colorSurfaceContainerLowest\"/>\n        <attr format=\"color\" name=\"colorSurfaceDim\"/>\n        <attr name=\"colorOutline\"/>\n        <attr name=\"colorOutlineVariant\"/>\n        <attr name=\"colorError\"/>\n        <attr name=\"colorOnError\"/>\n        <attr name=\"colorErrorContainer\"/>\n        <attr name=\"colorOnErrorContainer\"/>\n        <attr name=\"scrimBackground\"/>\n\n        <attr name=\"fontFamily\"/>\n        <attr name=\"android:fontFamily\"/>\n        <attr name=\"textAppearanceDisplayLarge\"/>\n        <attr name=\"textAppearanceDisplayMedium\"/>\n        <attr name=\"textAppearanceDisplaySmall\"/>\n        <attr name=\"textAppearanceHeadlineLarge\"/>\n        <attr name=\"textAppearanceHeadlineMedium\"/>\n        <attr name=\"textAppearanceHeadlineSmall\"/>\n        <attr name=\"textAppearanceTitleLarge\"/>\n        <attr name=\"textAppearanceTitleMedium\"/>\n        <attr name=\"textAppearanceTitleSmall\"/>\n        <attr name=\"textAppearanceBodyLarge\"/>\n        <attr name=\"textAppearanceBodyMedium\"/>\n        <attr name=\"textAppearanceBodySmall\"/>\n        <attr name=\"textAppearanceLabelLarge\"/>\n        <attr name=\"textAppearanceLabelMedium\"/>\n        <attr name=\"textAppearanceLabelSmall\"/>\n\n        <attr name=\"shapeAppearanceCornerExtraSmall\"/>\n        <attr name=\"shapeAppearanceCornerSmall\"/>\n        <attr name=\"shapeAppearanceCornerMedium\"/>\n        <attr name=\"shapeAppearanceCornerLarge\"/>\n        <attr name=\"shapeAppearanceCornerExtraLarge\"/>\n\n        <attr name=\"isLightTheme\"/>\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string-array name=\"preference_dark_mode_values\">\n        <item>1</item>\n        <item>2</item>\n        <item>-1</item>\n        <item>3</item>\n    </string-array>\n\n    <array name=\"stats_chart_colors\">\n        <item>#FEB700</item>\n        <item>#FF9300</item>\n        <item>#FF3281</item>\n        <item>#BC6EDA</item>\n        <item>#00BBED</item>\n        <item>#545C97</item>\n        <item>#EA6200</item>\n    </array>\n\n    <array name=\"ratings_chart_colors\">\n        <item>#ba2020</item>\n        <item>#ba2d20</item>\n        <item>#ba3920</item>\n        <item>#ba4620</item>\n        <item>#ba5320</item>\n        <item>#ba6020</item>\n        <item>#ba6d20</item>\n        <item>#ba7a20</item>\n        <item>#ba8720</item>\n        <item>#ba9420</item>\n        <item>#baa020</item>\n        <item>#baad20</item>\n        <item>#baba20</item>\n        <item>#adba20</item>\n        <item>#a0ba20</item>\n        <item>#94ba20</item>\n        <item>#87ba20</item>\n        <item>#7aba20</item>\n        <item>#6dba20</item>\n        <item>#60ba20</item>\n    </array>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/attr.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <attr name=\"fullScreenDialogTheme\" format=\"reference\" />\n\n    <attr name=\"colorPlaceholderGradientStart\" format=\"color\" />\n    <attr name=\"colorPlaceholderGradientEnd\" format=\"color\" />\n\n    <declare-styleable name=\"ExpandableLayout\">\n        <attr name=\"duration\" format=\"integer\" />\n        <attr name=\"expanded\" format=\"boolean\" />\n        <attr name=\"min_height\" format=\"dimension\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"CustomNumberSpinner\">\n        <attr name=\"suffixMode\" format=\"enum\">\n            <!-- Disable suffix text (default) -->\n            <enum name=\"disabled\" value=\"0\" />\n            <!-- Show max value in suffix -->\n            <enum name=\"maxValue\" value=\"1\" />\n            <!-- Use custom suffix text -->\n            <enum name=\"customText\" value=\"2\" />\n        </attr>\n        <!-- Custom suffix text -->\n        <attr name=\"suffixText\" format=\"string\" />\n\n        <attr name=\"enableAction\" format=\"boolean\" />\n        <attr name=\"actionText\" format=\"string\" />\n        <attr name=\"actionTooltip\" format=\"string\" />\n\n        <attr name=\"value\" format=\"integer\" />\n        <attr name=\"maxValue\" format=\"integer\" />\n        <attr name=\"minValue\" format=\"integer\" />\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#312631</color>\n    <color name=\"ic_shortcut_fill\">#6f5064</color>\n\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n\n    <color name=\"color_star\">#F39C12</color>\n    <color name=\"color_heart\">#E74C3C</color>\n    <color name=\"color_score\">#1BBC9C</color>\n\n    <color name=\"edge_fade_color\">#BF000000</color>\n    <color name=\"translucent_system_overlay\">#4D000000</color>\n\n    <color name=\"black_background\">@color/black</color>\n\n    <!-- M3 Default Theme Colors -->\n    <color name=\"seed\">#e23300</color>\n    <color name=\"md_theme_primary\">#904B3F</color>\n    <color name=\"md_theme_onPrimary\">#FFFFFF</color>\n    <color name=\"md_theme_primaryContainer\">#FFDAD4</color>\n    <color name=\"md_theme_onPrimaryContainer\">#3A0A04</color>\n    <color name=\"md_theme_secondary\">#775651</color>\n    <color name=\"md_theme_onSecondary\">#FFFFFF</color>\n    <color name=\"md_theme_secondaryContainer\">#FFDAD4</color>\n    <color name=\"md_theme_onSecondaryContainer\">#2C1511</color>\n    <color name=\"md_theme_tertiary\">#6F5C2E</color>\n    <color name=\"md_theme_onTertiary\">#FFFFFF</color>\n    <color name=\"md_theme_tertiaryContainer\">#FAE0A6</color>\n    <color name=\"md_theme_onTertiaryContainer\">#251A00</color>\n    <color name=\"md_theme_error\">#BA1A1A</color>\n    <color name=\"md_theme_onError\">#FFFFFF</color>\n    <color name=\"md_theme_errorContainer\">#FFDAD6</color>\n    <color name=\"md_theme_onErrorContainer\">#410002</color>\n    <color name=\"md_theme_background\">#FFF8F6</color>\n    <color name=\"md_theme_onBackground\">#231918</color>\n    <color name=\"md_theme_surface\">#FFF8F6</color>\n    <color name=\"md_theme_onSurface\">#231918</color>\n    <color name=\"md_theme_surfaceVariant\">#F5DDD9</color>\n    <color name=\"md_theme_onSurfaceVariant\">#534340</color>\n    <color name=\"md_theme_outline\">#857370</color>\n    <color name=\"md_theme_outlineVariant\">#D8C2BE</color>\n    <color name=\"md_theme_scrim\">#000000</color>\n    <color name=\"md_theme_inverseSurface\">#392E2C</color>\n    <color name=\"md_theme_inverseOnSurface\">#FFEDEA</color>\n    <color name=\"md_theme_inversePrimary\">#FFB4A7</color>\n    <color name=\"md_theme_primaryFixed\">#FFDAD4</color>\n    <color name=\"md_theme_onPrimaryFixed\">#3A0A04</color>\n    <color name=\"md_theme_primaryFixedDim\">#FFB4A7</color>\n    <color name=\"md_theme_onPrimaryFixedVariant\">#733429</color>\n    <color name=\"md_theme_secondaryFixed\">#FFDAD4</color>\n    <color name=\"md_theme_onSecondaryFixed\">#2C1511</color>\n    <color name=\"md_theme_secondaryFixedDim\">#E7BDB5</color>\n    <color name=\"md_theme_onSecondaryFixedVariant\">#5D3F3A</color>\n    <color name=\"md_theme_tertiaryFixed\">#FAE0A6</color>\n    <color name=\"md_theme_onTertiaryFixed\">#251A00</color>\n    <color name=\"md_theme_tertiaryFixedDim\">#DDC48C</color>\n    <color name=\"md_theme_onTertiaryFixedVariant\">#564519</color>\n    <color name=\"md_theme_surfaceDim\">#E8D6D3</color>\n    <color name=\"md_theme_surfaceBright\">#FFF8F6</color>\n    <color name=\"md_theme_surfaceContainerLowest\">#FFFFFF</color>\n    <color name=\"md_theme_surfaceContainerLow\">#FFF0EE</color>\n    <color name=\"md_theme_surfaceContainer\">#FCEAE7</color>\n    <color name=\"md_theme_surfaceContainerHigh\">#F7E4E1</color>\n    <color name=\"md_theme_surfaceContainerHighest\">#F1DFDB</color>\n    <color name=\"md_theme_primary_mediumContrast\">#6E3026</color>\n    <color name=\"md_theme_onPrimary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_primaryContainer_mediumContrast\">#AA6053</color>\n    <color name=\"md_theme_onPrimaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_secondary_mediumContrast\">#593B36</color>\n    <color name=\"md_theme_onSecondary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_secondaryContainer_mediumContrast\">#8F6C66</color>\n    <color name=\"md_theme_onSecondaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_tertiary_mediumContrast\">#514115</color>\n    <color name=\"md_theme_onTertiary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_tertiaryContainer_mediumContrast\">#877242</color>\n    <color name=\"md_theme_onTertiaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_error_mediumContrast\">#8C0009</color>\n    <color name=\"md_theme_onError_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_errorContainer_mediumContrast\">#DA342E</color>\n    <color name=\"md_theme_onErrorContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_background_mediumContrast\">#FFF8F6</color>\n    <color name=\"md_theme_onBackground_mediumContrast\">#231918</color>\n    <color name=\"md_theme_surface_mediumContrast\">#FFF8F6</color>\n    <color name=\"md_theme_onSurface_mediumContrast\">#231918</color>\n    <color name=\"md_theme_surfaceVariant_mediumContrast\">#F5DDD9</color>\n    <color name=\"md_theme_onSurfaceVariant_mediumContrast\">#4F3F3D</color>\n    <color name=\"md_theme_outline_mediumContrast\">#6C5B58</color>\n    <color name=\"md_theme_outlineVariant_mediumContrast\">#897773</color>\n    <color name=\"md_theme_scrim_mediumContrast\">#000000</color>\n    <color name=\"md_theme_inverseSurface_mediumContrast\">#392E2C</color>\n    <color name=\"md_theme_inverseOnSurface_mediumContrast\">#FFEDEA</color>\n    <color name=\"md_theme_inversePrimary_mediumContrast\">#FFB4A7</color>\n    <color name=\"md_theme_primaryFixed_mediumContrast\">#AA6053</color>\n    <color name=\"md_theme_onPrimaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_primaryFixedDim_mediumContrast\">#8D483C</color>\n    <color name=\"md_theme_onPrimaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_secondaryFixed_mediumContrast\">#8F6C66</color>\n    <color name=\"md_theme_onSecondaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_secondaryFixedDim_mediumContrast\">#75544E</color>\n    <color name=\"md_theme_onSecondaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_tertiaryFixed_mediumContrast\">#877242</color>\n    <color name=\"md_theme_onTertiaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_tertiaryFixedDim_mediumContrast\">#6C5A2C</color>\n    <color name=\"md_theme_onTertiaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_surfaceDim_mediumContrast\">#E8D6D3</color>\n    <color name=\"md_theme_surfaceBright_mediumContrast\">#FFF8F6</color>\n    <color name=\"md_theme_surfaceContainerLowest_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_theme_surfaceContainerLow_mediumContrast\">#FFF0EE</color>\n    <color name=\"md_theme_surfaceContainer_mediumContrast\">#FCEAE7</color>\n    <color name=\"md_theme_surfaceContainerHigh_mediumContrast\">#F7E4E1</color>\n    <color name=\"md_theme_surfaceContainerHighest_mediumContrast\">#F1DFDB</color>\n    <color name=\"md_theme_primary_highContrast\">#431009</color>\n    <color name=\"md_theme_onPrimary_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_primaryContainer_highContrast\">#6E3026</color>\n    <color name=\"md_theme_onPrimaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_secondary_highContrast\">#341C17</color>\n    <color name=\"md_theme_onSecondary_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_secondaryContainer_highContrast\">#593B36</color>\n    <color name=\"md_theme_onSecondaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_tertiary_highContrast\">#2D2000</color>\n    <color name=\"md_theme_onTertiary_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_tertiaryContainer_highContrast\">#514115</color>\n    <color name=\"md_theme_onTertiaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_error_highContrast\">#4E0002</color>\n    <color name=\"md_theme_onError_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_errorContainer_highContrast\">#8C0009</color>\n    <color name=\"md_theme_onErrorContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_background_highContrast\">#FFF8F6</color>\n    <color name=\"md_theme_onBackground_highContrast\">#231918</color>\n    <color name=\"md_theme_surface_highContrast\">#FFF8F6</color>\n    <color name=\"md_theme_onSurface_highContrast\">#000000</color>\n    <color name=\"md_theme_surfaceVariant_highContrast\">#F5DDD9</color>\n    <color name=\"md_theme_onSurfaceVariant_highContrast\">#2E211E</color>\n    <color name=\"md_theme_outline_highContrast\">#4F3F3D</color>\n    <color name=\"md_theme_outlineVariant_highContrast\">#4F3F3D</color>\n    <color name=\"md_theme_scrim_highContrast\">#000000</color>\n    <color name=\"md_theme_inverseSurface_highContrast\">#392E2C</color>\n    <color name=\"md_theme_inverseOnSurface_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_inversePrimary_highContrast\">#FFE7E2</color>\n    <color name=\"md_theme_primaryFixed_highContrast\">#6E3026</color>\n    <color name=\"md_theme_onPrimaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_primaryFixedDim_highContrast\">#511B12</color>\n    <color name=\"md_theme_onPrimaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_secondaryFixed_highContrast\">#593B36</color>\n    <color name=\"md_theme_onSecondaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_secondaryFixedDim_highContrast\">#402621</color>\n    <color name=\"md_theme_onSecondaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_tertiaryFixed_highContrast\">#514115</color>\n    <color name=\"md_theme_onTertiaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_tertiaryFixedDim_highContrast\">#392B02</color>\n    <color name=\"md_theme_onTertiaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_surfaceDim_highContrast\">#E8D6D3</color>\n    <color name=\"md_theme_surfaceBright_highContrast\">#FFF8F6</color>\n    <color name=\"md_theme_surfaceContainerLowest_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_surfaceContainerLow_highContrast\">#FFF0EE</color>\n    <color name=\"md_theme_surfaceContainer_highContrast\">#FCEAE7</color>\n    <color name=\"md_theme_surfaceContainerHigh_highContrast\">#F7E4E1</color>\n    <color name=\"md_theme_surfaceContainerHighest_highContrast\">#F1DFDB</color>\n\n    <!-- M3 Purple Theme Colors -->\n    <color name=\"purple_seed\">#503d4f</color>\n    <color name=\"md_purple_theme_primary\">#7D4E7E</color>\n    <color name=\"md_purple_theme_onPrimary\">#FFFFFF</color>\n    <color name=\"md_purple_theme_primaryContainer\">#FFD6FB</color>\n    <color name=\"md_purple_theme_onPrimaryContainer\">#320936</color>\n    <color name=\"md_purple_theme_secondary\">#6C586A</color>\n    <color name=\"md_purple_theme_onSecondary\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondaryContainer\">#F5DBF1</color>\n    <color name=\"md_purple_theme_onSecondaryContainer\">#261626</color>\n    <color name=\"md_purple_theme_tertiary\">#825249</color>\n    <color name=\"md_purple_theme_onTertiary\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiaryContainer\">#FFDAD3</color>\n    <color name=\"md_purple_theme_onTertiaryContainer\">#33110B</color>\n    <color name=\"md_purple_theme_error\">#BA1A1A</color>\n    <color name=\"md_purple_theme_onError\">#FFFFFF</color>\n    <color name=\"md_purple_theme_errorContainer\">#FFDAD6</color>\n    <color name=\"md_purple_theme_onErrorContainer\">#410002</color>\n    <color name=\"md_purple_theme_background\">#FFF7FA</color>\n    <color name=\"md_purple_theme_onBackground\">#1F1A1F</color>\n    <color name=\"md_purple_theme_surface\">#FFF7FA</color>\n    <color name=\"md_purple_theme_onSurface\">#1F1A1F</color>\n    <color name=\"md_purple_theme_surfaceVariant\">#EDDEE8</color>\n    <color name=\"md_purple_theme_onSurfaceVariant\">#4D444C</color>\n    <color name=\"md_purple_theme_outline\">#7F747C</color>\n    <color name=\"md_purple_theme_outlineVariant\">#D0C3CC</color>\n    <color name=\"md_purple_theme_scrim\">#000000</color>\n    <color name=\"md_purple_theme_inverseSurface\">#352E34</color>\n    <color name=\"md_purple_theme_inverseOnSurface\">#F9EEF4</color>\n    <color name=\"md_purple_theme_inversePrimary\">#EDB4EB</color>\n    <color name=\"md_purple_theme_primaryFixed\">#FFD6FB</color>\n    <color name=\"md_purple_theme_onPrimaryFixed\">#320936</color>\n    <color name=\"md_purple_theme_primaryFixedDim\">#EDB4EB</color>\n    <color name=\"md_purple_theme_onPrimaryFixedVariant\">#633664</color>\n    <color name=\"md_purple_theme_secondaryFixed\">#F5DBF1</color>\n    <color name=\"md_purple_theme_onSecondaryFixed\">#261626</color>\n    <color name=\"md_purple_theme_secondaryFixedDim\">#D8BFD4</color>\n    <color name=\"md_purple_theme_onSecondaryFixedVariant\">#534152</color>\n    <color name=\"md_purple_theme_tertiaryFixed\">#FFDAD3</color>\n    <color name=\"md_purple_theme_onTertiaryFixed\">#33110B</color>\n    <color name=\"md_purple_theme_tertiaryFixedDim\">#F6B8AC</color>\n    <color name=\"md_purple_theme_onTertiaryFixedVariant\">#673B33</color>\n    <color name=\"md_purple_theme_surfaceDim\">#E2D7DE</color>\n    <color name=\"md_purple_theme_surfaceBright\">#FFF7FA</color>\n    <color name=\"md_purple_theme_surfaceContainerLowest\">#FFFFFF</color>\n    <color name=\"md_purple_theme_surfaceContainerLow\">#FCF0F7</color>\n    <color name=\"md_purple_theme_surfaceContainer\">#F6EBF1</color>\n    <color name=\"md_purple_theme_surfaceContainerHigh\">#F1E5EC</color>\n    <color name=\"md_purple_theme_surfaceContainerHighest\">#EBDFE6</color>\n    <color name=\"md_purple_theme_primary_mediumContrast\">#5E3360</color>\n    <color name=\"md_purple_theme_onPrimary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_primaryContainer_mediumContrast\">#956495</color>\n    <color name=\"md_purple_theme_onPrimaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondary_mediumContrast\">#4F3D4E</color>\n    <color name=\"md_purple_theme_onSecondary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondaryContainer_mediumContrast\">#836E81</color>\n    <color name=\"md_purple_theme_onSecondaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiary_mediumContrast\">#62372F</color>\n    <color name=\"md_purple_theme_onTertiary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiaryContainer_mediumContrast\">#9B685E</color>\n    <color name=\"md_purple_theme_onTertiaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_error_mediumContrast\">#8C0009</color>\n    <color name=\"md_purple_theme_onError_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_errorContainer_mediumContrast\">#DA342E</color>\n    <color name=\"md_purple_theme_onErrorContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_background_mediumContrast\">#FFF7FA</color>\n    <color name=\"md_purple_theme_onBackground_mediumContrast\">#1F1A1F</color>\n    <color name=\"md_purple_theme_surface_mediumContrast\">#FFF7FA</color>\n    <color name=\"md_purple_theme_onSurface_mediumContrast\">#1F1A1F</color>\n    <color name=\"md_purple_theme_surfaceVariant_mediumContrast\">#EDDEE8</color>\n    <color name=\"md_purple_theme_onSurfaceVariant_mediumContrast\">#494048</color>\n    <color name=\"md_purple_theme_outline_mediumContrast\">#665C64</color>\n    <color name=\"md_purple_theme_outlineVariant_mediumContrast\">#837780</color>\n    <color name=\"md_purple_theme_scrim_mediumContrast\">#000000</color>\n    <color name=\"md_purple_theme_inverseSurface_mediumContrast\">#352E34</color>\n    <color name=\"md_purple_theme_inverseOnSurface_mediumContrast\">#F9EEF4</color>\n    <color name=\"md_purple_theme_inversePrimary_mediumContrast\">#EDB4EB</color>\n    <color name=\"md_purple_theme_primaryFixed_mediumContrast\">#956495</color>\n    <color name=\"md_purple_theme_onPrimaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_primaryFixedDim_mediumContrast\">#7A4B7B</color>\n    <color name=\"md_purple_theme_onPrimaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondaryFixed_mediumContrast\">#836E81</color>\n    <color name=\"md_purple_theme_onSecondaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondaryFixedDim_mediumContrast\">#695668</color>\n    <color name=\"md_purple_theme_onSecondaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiaryFixed_mediumContrast\">#9B685E</color>\n    <color name=\"md_purple_theme_onTertiaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiaryFixedDim_mediumContrast\">#7F5046</color>\n    <color name=\"md_purple_theme_onTertiaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_surfaceDim_mediumContrast\">#E2D7DE</color>\n    <color name=\"md_purple_theme_surfaceBright_mediumContrast\">#FFF7FA</color>\n    <color name=\"md_purple_theme_surfaceContainerLowest_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_surfaceContainerLow_mediumContrast\">#FCF0F7</color>\n    <color name=\"md_purple_theme_surfaceContainer_mediumContrast\">#F6EBF1</color>\n    <color name=\"md_purple_theme_surfaceContainerHigh_mediumContrast\">#F1E5EC</color>\n    <color name=\"md_purple_theme_surfaceContainerHighest_mediumContrast\">#EBDFE6</color>\n    <color name=\"md_purple_theme_primary_highContrast\">#39113D</color>\n    <color name=\"md_purple_theme_onPrimary_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_primaryContainer_highContrast\">#5E3360</color>\n    <color name=\"md_purple_theme_onPrimaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondary_highContrast\">#2D1D2C</color>\n    <color name=\"md_purple_theme_onSecondary_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondaryContainer_highContrast\">#4F3D4E</color>\n    <color name=\"md_purple_theme_onSecondaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiary_highContrast\">#3B1811</color>\n    <color name=\"md_purple_theme_onTertiary_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiaryContainer_highContrast\">#62372F</color>\n    <color name=\"md_purple_theme_onTertiaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_error_highContrast\">#4E0002</color>\n    <color name=\"md_purple_theme_onError_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_errorContainer_highContrast\">#8C0009</color>\n    <color name=\"md_purple_theme_onErrorContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_background_highContrast\">#FFF7FA</color>\n    <color name=\"md_purple_theme_onBackground_highContrast\">#1F1A1F</color>\n    <color name=\"md_purple_theme_surface_highContrast\">#FFF7FA</color>\n    <color name=\"md_purple_theme_onSurface_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_surfaceVariant_highContrast\">#EDDEE8</color>\n    <color name=\"md_purple_theme_onSurfaceVariant_highContrast\">#292228</color>\n    <color name=\"md_purple_theme_outline_highContrast\">#494048</color>\n    <color name=\"md_purple_theme_outlineVariant_highContrast\">#494048</color>\n    <color name=\"md_purple_theme_scrim_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_inverseSurface_highContrast\">#352E34</color>\n    <color name=\"md_purple_theme_inverseOnSurface_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_inversePrimary_highContrast\">#FFE4FA</color>\n    <color name=\"md_purple_theme_primaryFixed_highContrast\">#5E3360</color>\n    <color name=\"md_purple_theme_onPrimaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_primaryFixedDim_highContrast\">#451C49</color>\n    <color name=\"md_purple_theme_onPrimaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondaryFixed_highContrast\">#4F3D4E</color>\n    <color name=\"md_purple_theme_onSecondaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_secondaryFixedDim_highContrast\">#382737</color>\n    <color name=\"md_purple_theme_onSecondaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiaryFixed_highContrast\">#62372F</color>\n    <color name=\"md_purple_theme_onTertiaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_tertiaryFixedDim_highContrast\">#48221A</color>\n    <color name=\"md_purple_theme_onTertiaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_surfaceDim_highContrast\">#E2D7DE</color>\n    <color name=\"md_purple_theme_surfaceBright_highContrast\">#FFF7FA</color>\n    <color name=\"md_purple_theme_surfaceContainerLowest_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_surfaceContainerLow_highContrast\">#FCF0F7</color>\n    <color name=\"md_purple_theme_surfaceContainer_highContrast\">#F6EBF1</color>\n    <color name=\"md_purple_theme_surfaceContainerHigh_highContrast\">#F1E5EC</color>\n    <color name=\"md_purple_theme_surfaceContainerHighest_highContrast\">#EBDFE6</color>\n\n    <!-- M3 Blue Theme Colors -->\n    <color name=\"blue_seed\">#91b7e7</color>\n    <color name=\"md_blue_theme_primary\">#37618E</color>\n    <color name=\"md_blue_theme_onPrimary\">#FFFFFF</color>\n    <color name=\"md_blue_theme_primaryContainer\">#D2E4FF</color>\n    <color name=\"md_blue_theme_onPrimaryContainer\">#001D36</color>\n    <color name=\"md_blue_theme_secondary\">#535F70</color>\n    <color name=\"md_blue_theme_onSecondary\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondaryContainer\">#D7E3F8</color>\n    <color name=\"md_blue_theme_onSecondaryContainer\">#101C2B</color>\n    <color name=\"md_blue_theme_tertiary\">#6B5778</color>\n    <color name=\"md_blue_theme_onTertiary\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiaryContainer\">#F3DAFF</color>\n    <color name=\"md_blue_theme_onTertiaryContainer\">#251431</color>\n    <color name=\"md_blue_theme_error\">#BA1A1A</color>\n    <color name=\"md_blue_theme_onError\">#FFFFFF</color>\n    <color name=\"md_blue_theme_errorContainer\">#FFDAD6</color>\n    <color name=\"md_blue_theme_onErrorContainer\">#410002</color>\n    <color name=\"md_blue_theme_background\">#F8F9FF</color>\n    <color name=\"md_blue_theme_onBackground\">#191C20</color>\n    <color name=\"md_blue_theme_surface\">#F8F9FF</color>\n    <color name=\"md_blue_theme_onSurface\">#191C20</color>\n    <color name=\"md_blue_theme_surfaceVariant\">#DFE2EB</color>\n    <color name=\"md_blue_theme_onSurfaceVariant\">#43474E</color>\n    <color name=\"md_blue_theme_outline\">#73777F</color>\n    <color name=\"md_blue_theme_outlineVariant\">#C3C6CF</color>\n    <color name=\"md_blue_theme_scrim\">#000000</color>\n    <color name=\"md_blue_theme_inverseSurface\">#2E3135</color>\n    <color name=\"md_blue_theme_inverseOnSurface\">#EFF0F7</color>\n    <color name=\"md_blue_theme_inversePrimary\">#A1CAFD</color>\n    <color name=\"md_blue_theme_primaryFixed\">#D2E4FF</color>\n    <color name=\"md_blue_theme_onPrimaryFixed\">#001D36</color>\n    <color name=\"md_blue_theme_primaryFixedDim\">#A1CAFD</color>\n    <color name=\"md_blue_theme_onPrimaryFixedVariant\">#1A4975</color>\n    <color name=\"md_blue_theme_secondaryFixed\">#D7E3F8</color>\n    <color name=\"md_blue_theme_onSecondaryFixed\">#101C2B</color>\n    <color name=\"md_blue_theme_secondaryFixedDim\">#BBC7DB</color>\n    <color name=\"md_blue_theme_onSecondaryFixedVariant\">#3C4858</color>\n    <color name=\"md_blue_theme_tertiaryFixed\">#F3DAFF</color>\n    <color name=\"md_blue_theme_onTertiaryFixed\">#251431</color>\n    <color name=\"md_blue_theme_tertiaryFixedDim\">#D7BEE4</color>\n    <color name=\"md_blue_theme_onTertiaryFixedVariant\">#533F5F</color>\n    <color name=\"md_blue_theme_surfaceDim\">#D8DAE0</color>\n    <color name=\"md_blue_theme_surfaceBright\">#F8F9FF</color>\n    <color name=\"md_blue_theme_surfaceContainerLowest\">#FFFFFF</color>\n    <color name=\"md_blue_theme_surfaceContainerLow\">#F2F3FA</color>\n    <color name=\"md_blue_theme_surfaceContainer\">#ECEEF4</color>\n    <color name=\"md_blue_theme_surfaceContainerHigh\">#E6E8EE</color>\n    <color name=\"md_blue_theme_surfaceContainerHighest\">#E1E2E8</color>\n    <color name=\"md_blue_theme_primary_mediumContrast\">#154571</color>\n    <color name=\"md_blue_theme_onPrimary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_primaryContainer_mediumContrast\">#4E77A6</color>\n    <color name=\"md_blue_theme_onPrimaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondary_mediumContrast\">#384454</color>\n    <color name=\"md_blue_theme_onSecondary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondaryContainer_mediumContrast\">#697587</color>\n    <color name=\"md_blue_theme_onSecondaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiary_mediumContrast\">#4E3B5B</color>\n    <color name=\"md_blue_theme_onTertiary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiaryContainer_mediumContrast\">#826D8F</color>\n    <color name=\"md_blue_theme_onTertiaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_error_mediumContrast\">#8C0009</color>\n    <color name=\"md_blue_theme_onError_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_errorContainer_mediumContrast\">#DA342E</color>\n    <color name=\"md_blue_theme_onErrorContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_background_mediumContrast\">#F8F9FF</color>\n    <color name=\"md_blue_theme_onBackground_mediumContrast\">#191C20</color>\n    <color name=\"md_blue_theme_surface_mediumContrast\">#F8F9FF</color>\n    <color name=\"md_blue_theme_onSurface_mediumContrast\">#191C20</color>\n    <color name=\"md_blue_theme_surfaceVariant_mediumContrast\">#DFE2EB</color>\n    <color name=\"md_blue_theme_onSurfaceVariant_mediumContrast\">#3F434A</color>\n    <color name=\"md_blue_theme_outline_mediumContrast\">#5B5F67</color>\n    <color name=\"md_blue_theme_outlineVariant_mediumContrast\">#777B83</color>\n    <color name=\"md_blue_theme_scrim_mediumContrast\">#000000</color>\n    <color name=\"md_blue_theme_inverseSurface_mediumContrast\">#2E3135</color>\n    <color name=\"md_blue_theme_inverseOnSurface_mediumContrast\">#EFF0F7</color>\n    <color name=\"md_blue_theme_inversePrimary_mediumContrast\">#A1CAFD</color>\n    <color name=\"md_blue_theme_primaryFixed_mediumContrast\">#4E77A6</color>\n    <color name=\"md_blue_theme_onPrimaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_primaryFixedDim_mediumContrast\">#345E8C</color>\n    <color name=\"md_blue_theme_onPrimaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondaryFixed_mediumContrast\">#697587</color>\n    <color name=\"md_blue_theme_onSecondaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondaryFixedDim_mediumContrast\">#515D6E</color>\n    <color name=\"md_blue_theme_onSecondaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiaryFixed_mediumContrast\">#826D8F</color>\n    <color name=\"md_blue_theme_onTertiaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiaryFixedDim_mediumContrast\">#695475</color>\n    <color name=\"md_blue_theme_onTertiaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_surfaceDim_mediumContrast\">#D8DAE0</color>\n    <color name=\"md_blue_theme_surfaceBright_mediumContrast\">#F8F9FF</color>\n    <color name=\"md_blue_theme_surfaceContainerLowest_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_surfaceContainerLow_mediumContrast\">#F2F3FA</color>\n    <color name=\"md_blue_theme_surfaceContainer_mediumContrast\">#ECEEF4</color>\n    <color name=\"md_blue_theme_surfaceContainerHigh_mediumContrast\">#E6E8EE</color>\n    <color name=\"md_blue_theme_surfaceContainerHighest_mediumContrast\">#E1E2E8</color>\n    <color name=\"md_blue_theme_primary_highContrast\">#002341</color>\n    <color name=\"md_blue_theme_onPrimary_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_primaryContainer_highContrast\">#154571</color>\n    <color name=\"md_blue_theme_onPrimaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondary_highContrast\">#172332</color>\n    <color name=\"md_blue_theme_onSecondary_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondaryContainer_highContrast\">#384454</color>\n    <color name=\"md_blue_theme_onSecondaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiary_highContrast\">#2C1B38</color>\n    <color name=\"md_blue_theme_onTertiary_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiaryContainer_highContrast\">#4E3B5B</color>\n    <color name=\"md_blue_theme_onTertiaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_error_highContrast\">#4E0002</color>\n    <color name=\"md_blue_theme_onError_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_errorContainer_highContrast\">#8C0009</color>\n    <color name=\"md_blue_theme_onErrorContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_background_highContrast\">#F8F9FF</color>\n    <color name=\"md_blue_theme_onBackground_highContrast\">#191C20</color>\n    <color name=\"md_blue_theme_surface_highContrast\">#F8F9FF</color>\n    <color name=\"md_blue_theme_onSurface_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_surfaceVariant_highContrast\">#DFE2EB</color>\n    <color name=\"md_blue_theme_onSurfaceVariant_highContrast\">#20242B</color>\n    <color name=\"md_blue_theme_outline_highContrast\">#3F434A</color>\n    <color name=\"md_blue_theme_outlineVariant_highContrast\">#3F434A</color>\n    <color name=\"md_blue_theme_scrim_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_inverseSurface_highContrast\">#2E3135</color>\n    <color name=\"md_blue_theme_inverseOnSurface_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_inversePrimary_highContrast\">#E2EDFF</color>\n    <color name=\"md_blue_theme_primaryFixed_highContrast\">#154571</color>\n    <color name=\"md_blue_theme_onPrimaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_primaryFixedDim_highContrast\">#002E53</color>\n    <color name=\"md_blue_theme_onPrimaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondaryFixed_highContrast\">#384454</color>\n    <color name=\"md_blue_theme_onSecondaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_secondaryFixedDim_highContrast\">#212E3D</color>\n    <color name=\"md_blue_theme_onSecondaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiaryFixed_highContrast\">#4E3B5B</color>\n    <color name=\"md_blue_theme_onTertiaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_tertiaryFixedDim_highContrast\">#372543</color>\n    <color name=\"md_blue_theme_onTertiaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_surfaceDim_highContrast\">#D8DAE0</color>\n    <color name=\"md_blue_theme_surfaceBright_highContrast\">#F8F9FF</color>\n    <color name=\"md_blue_theme_surfaceContainerLowest_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_surfaceContainerLow_highContrast\">#F2F3FA</color>\n    <color name=\"md_blue_theme_surfaceContainer_highContrast\">#ECEEF4</color>\n    <color name=\"md_blue_theme_surfaceContainerHigh_highContrast\">#E6E8EE</color>\n    <color name=\"md_blue_theme_surfaceContainerHighest_highContrast\">#E1E2E8</color>\n\n    <!-- M3 Green Theme Colors -->\n    <color name=\"green_seed\">#415e54</color>\n    <color name=\"md_green_theme_primary\">#126B56</color>\n    <color name=\"md_green_theme_onPrimary\">#FFFFFF</color>\n    <color name=\"md_green_theme_primaryContainer\">#A3F2D8</color>\n    <color name=\"md_green_theme_onPrimaryContainer\">#002018</color>\n    <color name=\"md_green_theme_secondary\">#4B635B</color>\n    <color name=\"md_green_theme_onSecondary\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondaryContainer\">#CEE9DD</color>\n    <color name=\"md_green_theme_onSecondaryContainer\">#072019</color>\n    <color name=\"md_green_theme_tertiary\">#416276</color>\n    <color name=\"md_green_theme_onTertiary\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiaryContainer\">#C4E7FF</color>\n    <color name=\"md_green_theme_onTertiaryContainer\">#001E2C</color>\n    <color name=\"md_green_theme_error\">#BA1A1A</color>\n    <color name=\"md_green_theme_onError\">#FFFFFF</color>\n    <color name=\"md_green_theme_errorContainer\">#FFDAD6</color>\n    <color name=\"md_green_theme_onErrorContainer\">#410002</color>\n    <color name=\"md_green_theme_background\">#F5FBF6</color>\n    <color name=\"md_green_theme_onBackground\">#171D1A</color>\n    <color name=\"md_green_theme_surface\">#F5FBF6</color>\n    <color name=\"md_green_theme_onSurface\">#171D1A</color>\n    <color name=\"md_green_theme_surfaceVariant\">#DBE5DF</color>\n    <color name=\"md_green_theme_onSurfaceVariant\">#3F4945</color>\n    <color name=\"md_green_theme_outline\">#6F7975</color>\n    <color name=\"md_green_theme_outlineVariant\">#BFC9C3</color>\n    <color name=\"md_green_theme_scrim\">#000000</color>\n    <color name=\"md_green_theme_inverseSurface\">#2B322F</color>\n    <color name=\"md_green_theme_inverseOnSurface\">#ECF2EE</color>\n    <color name=\"md_green_theme_inversePrimary\">#87D6BC</color>\n    <color name=\"md_green_theme_primaryFixed\">#A3F2D8</color>\n    <color name=\"md_green_theme_onPrimaryFixed\">#002018</color>\n    <color name=\"md_green_theme_primaryFixedDim\">#87D6BC</color>\n    <color name=\"md_green_theme_onPrimaryFixedVariant\">#005140</color>\n    <color name=\"md_green_theme_secondaryFixed\">#CEE9DD</color>\n    <color name=\"md_green_theme_onSecondaryFixed\">#072019</color>\n    <color name=\"md_green_theme_secondaryFixedDim\">#B2CCC1</color>\n    <color name=\"md_green_theme_onSecondaryFixedVariant\">#344C43</color>\n    <color name=\"md_green_theme_tertiaryFixed\">#C4E7FF</color>\n    <color name=\"md_green_theme_onTertiaryFixed\">#001E2C</color>\n    <color name=\"md_green_theme_tertiaryFixedDim\">#A8CBE2</color>\n    <color name=\"md_green_theme_onTertiaryFixedVariant\">#284B5E</color>\n    <color name=\"md_green_theme_surfaceDim\">#D5DBD7</color>\n    <color name=\"md_green_theme_surfaceBright\">#F5FBF6</color>\n    <color name=\"md_green_theme_surfaceContainerLowest\">#FFFFFF</color>\n    <color name=\"md_green_theme_surfaceContainerLow\">#EFF5F1</color>\n    <color name=\"md_green_theme_surfaceContainer\">#E9EFEB</color>\n    <color name=\"md_green_theme_surfaceContainerHigh\">#E3EAE5</color>\n    <color name=\"md_green_theme_surfaceContainerHighest\">#DEE4E0</color>\n    <color name=\"md_green_theme_primary_mediumContrast\">#004C3C</color>\n    <color name=\"md_green_theme_onPrimary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_primaryContainer_mediumContrast\">#31826C</color>\n    <color name=\"md_green_theme_onPrimaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondary_mediumContrast\">#30483F</color>\n    <color name=\"md_green_theme_onSecondary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondaryContainer_mediumContrast\">#617A70</color>\n    <color name=\"md_green_theme_onSecondaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiary_mediumContrast\">#244759</color>\n    <color name=\"md_green_theme_onTertiary_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiaryContainer_mediumContrast\">#57798D</color>\n    <color name=\"md_green_theme_onTertiaryContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_error_mediumContrast\">#8C0009</color>\n    <color name=\"md_green_theme_onError_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_errorContainer_mediumContrast\">#DA342E</color>\n    <color name=\"md_green_theme_onErrorContainer_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_background_mediumContrast\">#F5FBF6</color>\n    <color name=\"md_green_theme_onBackground_mediumContrast\">#171D1A</color>\n    <color name=\"md_green_theme_surface_mediumContrast\">#F5FBF6</color>\n    <color name=\"md_green_theme_onSurface_mediumContrast\">#171D1A</color>\n    <color name=\"md_green_theme_surfaceVariant_mediumContrast\">#DBE5DF</color>\n    <color name=\"md_green_theme_onSurfaceVariant_mediumContrast\">#3B4541</color>\n    <color name=\"md_green_theme_outline_mediumContrast\">#58615D</color>\n    <color name=\"md_green_theme_outlineVariant_mediumContrast\">#737D78</color>\n    <color name=\"md_green_theme_scrim_mediumContrast\">#000000</color>\n    <color name=\"md_green_theme_inverseSurface_mediumContrast\">#2B322F</color>\n    <color name=\"md_green_theme_inverseOnSurface_mediumContrast\">#ECF2EE</color>\n    <color name=\"md_green_theme_inversePrimary_mediumContrast\">#87D6BC</color>\n    <color name=\"md_green_theme_primaryFixed_mediumContrast\">#31826C</color>\n    <color name=\"md_green_theme_onPrimaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_primaryFixedDim_mediumContrast\">#0C6854</color>\n    <color name=\"md_green_theme_onPrimaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondaryFixed_mediumContrast\">#617A70</color>\n    <color name=\"md_green_theme_onSecondaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondaryFixedDim_mediumContrast\">#496158</color>\n    <color name=\"md_green_theme_onSecondaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiaryFixed_mediumContrast\">#57798D</color>\n    <color name=\"md_green_theme_onTertiaryFixed_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiaryFixedDim_mediumContrast\">#3E6074</color>\n    <color name=\"md_green_theme_onTertiaryFixedVariant_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_surfaceDim_mediumContrast\">#D5DBD7</color>\n    <color name=\"md_green_theme_surfaceBright_mediumContrast\">#F5FBF6</color>\n    <color name=\"md_green_theme_surfaceContainerLowest_mediumContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_surfaceContainerLow_mediumContrast\">#EFF5F1</color>\n    <color name=\"md_green_theme_surfaceContainer_mediumContrast\">#E9EFEB</color>\n    <color name=\"md_green_theme_surfaceContainerHigh_mediumContrast\">#E3EAE5</color>\n    <color name=\"md_green_theme_surfaceContainerHighest_mediumContrast\">#DEE4E0</color>\n    <color name=\"md_green_theme_primary_highContrast\">#00281E</color>\n    <color name=\"md_green_theme_onPrimary_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_primaryContainer_highContrast\">#004C3C</color>\n    <color name=\"md_green_theme_onPrimaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondary_highContrast\">#0F261F</color>\n    <color name=\"md_green_theme_onSecondary_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondaryContainer_highContrast\">#30483F</color>\n    <color name=\"md_green_theme_onSecondaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiary_highContrast\">#002536</color>\n    <color name=\"md_green_theme_onTertiary_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiaryContainer_highContrast\">#244759</color>\n    <color name=\"md_green_theme_onTertiaryContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_error_highContrast\">#4E0002</color>\n    <color name=\"md_green_theme_onError_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_errorContainer_highContrast\">#8C0009</color>\n    <color name=\"md_green_theme_onErrorContainer_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_background_highContrast\">#F5FBF6</color>\n    <color name=\"md_green_theme_onBackground_highContrast\">#171D1A</color>\n    <color name=\"md_green_theme_surface_highContrast\">#F5FBF6</color>\n    <color name=\"md_green_theme_onSurface_highContrast\">#000000</color>\n    <color name=\"md_green_theme_surfaceVariant_highContrast\">#DBE5DF</color>\n    <color name=\"md_green_theme_onSurfaceVariant_highContrast\">#1D2622</color>\n    <color name=\"md_green_theme_outline_highContrast\">#3B4541</color>\n    <color name=\"md_green_theme_outlineVariant_highContrast\">#3B4541</color>\n    <color name=\"md_green_theme_scrim_highContrast\">#000000</color>\n    <color name=\"md_green_theme_inverseSurface_highContrast\">#2B322F</color>\n    <color name=\"md_green_theme_inverseOnSurface_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_inversePrimary_highContrast\">#ACFCE1</color>\n    <color name=\"md_green_theme_primaryFixed_highContrast\">#004C3C</color>\n    <color name=\"md_green_theme_onPrimaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_primaryFixedDim_highContrast\">#003428</color>\n    <color name=\"md_green_theme_onPrimaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondaryFixed_highContrast\">#30483F</color>\n    <color name=\"md_green_theme_onSecondaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_secondaryFixedDim_highContrast\">#1A312A</color>\n    <color name=\"md_green_theme_onSecondaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiaryFixed_highContrast\">#244759</color>\n    <color name=\"md_green_theme_onTertiaryFixed_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_tertiaryFixedDim_highContrast\">#083042</color>\n    <color name=\"md_green_theme_onTertiaryFixedVariant_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_surfaceDim_highContrast\">#D5DBD7</color>\n    <color name=\"md_green_theme_surfaceBright_highContrast\">#F5FBF6</color>\n    <color name=\"md_green_theme_surfaceContainerLowest_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_surfaceContainerLow_highContrast\">#EFF5F1</color>\n    <color name=\"md_green_theme_surfaceContainer_highContrast\">#E9EFEB</color>\n    <color name=\"md_green_theme_surfaceContainerHigh_highContrast\">#E3EAE5</color>\n    <color name=\"md_green_theme_surfaceContainerHighest_highContrast\">#DEE4E0</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <dimen name=\"zero_dp\">0dp</dimen>\n\n    <!-- Media item card -->\n    <dimen name=\"media_item_width_small\">106dp</dimen>\n    <dimen name=\"media_item_height_small\">150dp</dimen>\n\n    <dimen name=\"media_item_width_medium\">141dp</dimen>\n    <dimen name=\"media_item_height_medium\">200dp</dimen>\n\n    <dimen name=\"media_item_width_large\">169dp</dimen>\n    <dimen name=\"media_item_height_large\">240dp</dimen>\n\n    <dimen name=\"media_item_margin\">5dp</dimen>\n\n    <!-- Library entry card -->\n    <dimen name=\"library_entry_img_width\">106dp</dimen>\n    <dimen name=\"library_entry_img_height\">150dp</dimen>\n\n    <!-- Edit Library Entry Fragment -->\n    <dimen name=\"edit_library_entry_img_width\">57dp</dimen>\n    <dimen name=\"edit_library_entry_img_height\">80dp</dimen>\n    <dimen name=\"edit_library_min_width\">200dp</dimen>\n\n    <!-- Streamer card -->\n    <dimen name=\"streamer_card_width\">100dp</dimen>\n    <dimen name=\"streamer_card_height\">100dp</dimen>\n\n    <!-- Episode card -->\n    <dimen name=\"episode_card_img_height\">110dp</dimen>\n    <dimen name=\"episode_card_img_width\">100dp</dimen>\n\n    <!-- Details Fragment -->\n    <dimen name=\"details_trailer_height\">150dp</dimen>\n    <dimen name=\"details_thumbnail_width\">150dp</dimen>\n    <dimen name=\"details_thumbnail_height\">213dp</dimen>\n    <dimen name=\"details_rating_chart_full_threshold\">400dp</dimen>\n\n    <!-- Profile Fragment -->\n    <dimen name=\"profile_pie_chart_height\">300dp</dimen>\n\n    <!-- Icon Button -->\n    <dimen name=\"icon_button_size\">38dp</dimen>\n    <dimen name=\"icon_button_padding\">6dp</dimen>\n\n    <dimen name=\"card_inner_padding\">16dp</dimen>\n\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n\n    <!-- Widget -->\n    <dimen name=\"widget_poster_width\">60dp</dimen>\n    <dimen name=\"widget_poster_height\">85dp</dimen>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/integers.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <integer name=\"bottom_navigation_animation_duration\">200</integer>\n    <integer name=\"navigation_rail_animation_duration\">80</integer>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/preference_keys.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Preference Files -->\n    <string name=\"preference_file_key\">app_preferences</string>\n    <string name=\"auth_preference_file_key\">auth_preferences</string>\n    <string name=\"user_preference_file_key\">user_preferences</string>\n\n    <!-- Preference Keys -->\n    <string name=\"preference_key_fragment_appearance\">fragment_appearance</string>\n    <string name=\"preference_key_app_theme\">app_theme</string>\n    <string name=\"preference_key_dynamic_color_theme\">dynamic_color_theme</string>\n    <string name=\"preference_key_dark_mode\">dark_mode</string>\n    <string name=\"preference_key_oled_black_mode\">oled_black_mode</string>\n    <string name=\"preference_key_media_item_size\">media_item_size</string>\n    <string name=\"preference_key_start_fragment\">start_fragment</string>\n    <string name=\"preference_key_language\">language</string>\n    <string name=\"preference_key_remember_search_filters\">remember_search_filters</string>\n    <string name=\"preference_key_force_legacy_image_picker\">force_legacy_image_picker</string>\n    <string name=\"preference_key_check_for_updates_on_start\">check_for_updates_on_start</string>\n    <string name=\"preference_key_app_logs\">app_logs</string>\n    <string name=\"preference_key_app_version\">app_version</string>\n    <string name=\"preference_key_open_source_libraries\">open_source_libraries</string>\n\n    <!-- Profile Preference Keys -->\n    <string name=\"preference_key_display_name\">display_name</string>\n    <string name=\"preference_key_profile_url\">profile_url</string>\n    <string name=\"preference_key_country\">country</string>\n    <string name=\"preference_key_titles\">titles</string>\n    <string name=\"preference_key_sfw_filter\">sfw_filter</string>\n    <string name=\"preference_key_rating_system\">rating_system</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" translatable=\"false\">Kitsune</string>\n    <string name=\"github_repo_url\" translatable=\"false\">https://github.com/Drumber/Kitsune</string>\n    <string name=\"nav_home\">Home</string>\n    <string name=\"nav_search\">Search</string>\n    <string name=\"nav_library\">Library</string>\n    <string name=\"nav_profile\">Profile</string>\n    <string name=\"nav_settings\">Settings</string>\n    <string name=\"nav_appearance\">Appearance</string>\n    <string name=\"nav_app_logs\">Application Logs</string>\n    <string name=\"nav_os_libraries\">Open Source Libraries</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"manga\">Manga</string>\n    <string name=\"action_retry\">Retry</string>\n    <string name=\"action_ok\">OK</string>\n    <string name=\"action_cancel\">Cancel</string>\n    <string name=\"action_reset_filter\">Reset filter</string>\n    <string name=\"action_unselect_all\">Unselect all</string>\n    <string name=\"action_read_more\">Read more</string>\n    <string name=\"action_read_less\">Read less</string>\n    <string name=\"action_show_more\">Show more</string>\n    <string name=\"action_show_less\">Show less</string>\n    <string name=\"action_view\">View</string>\n    <string name=\"action_back\">Back</string>\n    <string name=\"action_skip\">Skip</string>\n    <string name=\"action_next\">Next</string>\n    <string name=\"action_continue\">Continue</string>\n    <string name=\"action_close\">Close</string>\n    <string name=\"action_log_out\">Log out</string>\n    <string name=\"action_share_profile_url\">Share Profile</string>\n    <string name=\"action_share\">Share</string>\n    <string name=\"action_open_external\">Open on an external site</string>\n    <string name=\"action_open_external_short\">External sites</string>\n    <string name=\"action_share_app_logs\">Share Logs</string>\n    <string name=\"action_add_to_favorites\">Add to favorites</string>\n    <string name=\"action_remove_from_favorites\">Remove from favorites</string>\n    <string name=\"action_dismiss\">Dismiss</string>\n    <string name=\"action_synchronize\">Synchronize</string>\n    <string name=\"action_save_in_gallery\">Save in Gallery</string>\n    <string name=\"action_open_in_browser\">Open in Browser</string>\n    <string name=\"action_copy_url\">Copy URL</string>\n    <string name=\"action_open_on_mal\">Open on MyAnimeList.net</string>\n    <string name=\"action_save_changes\">Save Changes</string>\n    <string name=\"action_edit_profile\">Edit Profile</string>\n    <string name=\"action_update_profile\">Update Profile</string>\n    <string name=\"action_add\">Add</string>\n    <string name=\"action_remove\">Remove</string>\n    <string name=\"action_update\">Update</string>\n    <string name=\"action_start\">Start</string>\n    <string name=\"action_allow\">Allow</string>\n    <string name=\"action_rate\">Rate</string>\n    <string name=\"action_update_rating\">Update Rating</string>\n    <string name=\"action_remove_rating\">Remove Rating</string>\n    <string name=\"action_add_profile_link\">Add Profile Link</string>\n    <string name=\"action_edit_profile_link\">Edit Profile Link</string>\n    <string name=\"action_select_profile_link_site\">Select a Site</string>\n    <string name=\"action_delete_profile_link\">Delete Profile Link</string>\n    <string name=\"hint_search\">Search</string>\n    <string name=\"hint_search_characters\">Search characters</string>\n    <string name=\"hint_mark_watched\">Mark as watched.</string>\n    <string name=\"hint_mark_not_watched\">Mark as not watched.</string>\n    <string name=\"hint_rating\">Rating</string>\n    <string name=\"dialog_log_out_confirmation\">Are you sure you want to log out?</string>\n    <string name=\"dialog_remove_from_library_title\">Remove from Library?</string>\n    <string name=\"dialog_remove_from_library_msg\">&lt;b&gt;%s&lt;/b&gt; will be removed from your library.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Permission rejected</string>\n    <string name=\"dialog_notification_permission_rejected\">You won\\'t get notifications and won\\'t be informed about app updates.</string>\n    <string name=\"dialog_request_notification_permission_title\">Allow notifications</string>\n    <string name=\"dialog_request_notification_permission\">Allow push notifications to get notified when a new app version is released.</string>\n    <string name=\"error_resource_loading\">Could not load data.</string>\n    <string name=\"error_image_loading\">Failed to load image.</string>\n    <string name=\"error_mapping_loading\">Failed to load mappings for external sites.</string>\n    <string name=\"error_nothing_found\">Nothing found.</string>\n    <string name=\"error_invalid_user\">Invalid User. Are you logged in?</string>\n    <string name=\"error_user_update_failed\">Failed to update user data.</string>\n    <string name=\"error_user_delete_waifu_failed\">Failed to delete Waifu/Husbando.</string>\n    <string name=\"error_user_update_image_failed\">Failed to update profile image or cover.</string>\n    <string name=\"error_user_create_profile_link_failed\">Failed to create profile link for %s.</string>\n    <string name=\"error_user_update_profile_link_failed\">Failed to update profile link for %s.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Failed to delete profile link for %s.</string>\n    <string name=\"error_something_wrong\">Oops, something went wrong!</string>\n    <string name=\"error_requires_external_storage_permission\">Require permission to write to external storage.</string>\n    <string name=\"error_search_provider_unavailable\">Search Provider unavailable.</string>\n    <string name=\"error_library_update_failed\">Failed to update library entry.</string>\n    <string name=\"error_library_update_failed_multiple\">Failed to update %d library entries.</string>\n    <string name=\"error_library_update_not_found\">The library entry does not exist.</string>\n    <string name=\"error_library_add_failed\">Failed to add to your library.</string>\n    <string name=\"error_library_delete_failed\">Failed to delete library entry.</string>\n    <string name=\"error_requires_notification_permission\">Notification permission is required.</string>\n    <string name=\"sort_popularity_asc\">Least Popular</string>\n    <string name=\"sort_popularity_desc\">Most Popular</string>\n    <string name=\"sort_average_rating_asc\">Worst Rated</string>\n    <string name=\"sort_average_rating_desc\">Best Rated</string>\n    <string name=\"sort_release_date_asc\">Oldest</string>\n    <string name=\"sort_release_date_desc\">Latest</string>\n    <string name=\"title_media_type\">Media Type</string>\n    <string name=\"title_character_appearances\">Character Appearances</string>\n    <string name=\"title_categories\">Categories</string>\n    <string name=\"title_filter\">Filter</string>\n    <string name=\"title_description\">Description</string>\n    <string name=\"title_details\">Details</string>\n    <string name=\"title_streamer\">Streaming Links</string>\n    <string name=\"title_ratings\">Ratings</string>\n    <string name=\"title_more_franchise\">More from this Series</string>\n    <string name=\"title_favorite_anime\">Favorite Anime</string>\n    <string name=\"title_favorite_manga\">Favorite Manga</string>\n    <string name=\"title_favorite_characters\">Favorite Characters</string>\n    <string name=\"title_authentication\">Authentication</string>\n    <string name=\"title_login\">Log in to your Kitsu account</string>\n    <string name=\"title_play_trailer\">Play Trailer</string>\n    <string name=\"title_about_me\">About Me</string>\n    <string name=\"title_episodes\">Episodes</string>\n    <string name=\"title_chapters\">Chapters</string>\n    <string name=\"title_characters\">Characters</string>\n    <string name=\"title_edit_library_entry\">Edit Library Entry</string>\n    <string name=\"title_edit_profile\">Edit Profile</string>\n    <string name=\"no_information\">Unknown</string>\n    <string name=\"no_data_available\">No Data available</string>\n    <string name=\"status_current\">Currently Airing</string>\n    <string name=\"status_current_manga\">Publishing</string>\n    <string name=\"status_finished\">Finished</string>\n    <string name=\"status_tba\">TBA</string>\n    <string name=\"status_unreleased\">Unreleased</string>\n    <string name=\"status_upcoming\">Upcoming</string>\n    <string name=\"character_role_main\">Main</string>\n    <string name=\"character_role_supporting\">Supporting</string>\n    <string name=\"character_role_recurring\">Recurring</string>\n    <string name=\"character_role_cameo\">Cameo</string>\n    <string name=\"library_status_watching\">Watching</string>\n    <string name=\"library_status_planned\">Want to Watch</string>\n    <string name=\"library_status_completed\">Completed</string>\n    <string name=\"library_status_on_hold\">On Hold</string>\n    <string name=\"library_status_dropped\">Dropped</string>\n    <string name=\"library_status_reading\">Reading</string>\n    <string name=\"library_status_planned_manga\">Want to Read</string>\n    <string name=\"library_action_remove\">Remove from Library</string>\n    <string name=\"library_action_add\">Add to Library</string>\n    <string name=\"library_action_edit\">Edit Library Entry</string>\n    <string name=\"data_rank\">Rank #%d</string>\n    <string name=\"data_popularity\">Popularity #%d</string>\n    <string name=\"data_kitsu_score\">Kitsu Score %s%%</string>\n    <string name=\"data_length_total\">%s total</string>\n    <string name=\"data_length_each\">%d minutes each</string>\n    <string name=\"data_title_english\">English</string>\n    <string name=\"data_title_japanese\">Japanese</string>\n    <string name=\"data_title_japanese_romaji\">Japanese (Romaji)</string>\n    <string name=\"data_abbreviated_titles\">Synonyms</string>\n    <string name=\"data_other_names\">Also known as</string>\n    <string name=\"data_title_type\">Type</string>\n    <string name=\"data_title_status\">Status</string>\n    <string name=\"data_title_aired\">Aired</string>\n    <string name=\"data_title_published\">Published</string>\n    <string name=\"data_title_tba\">Expected</string>\n    <string name=\"data_title_season\">Season</string>\n    <string name=\"data_title_age_rating\">Rating</string>\n    <string name=\"data_title_serialization\">Serialization</string>\n    <string name=\"data_title_chapters\">Chapters</string>\n    <string name=\"data_title_volumes\">Volumes</string>\n    <string name=\"data_title_episodes\">Episodes</string>\n    <string name=\"data_title_length\">Length</string>\n    <string name=\"data_title_producers\">Producers</string>\n    <string name=\"data_title_licensors\">Licensors</string>\n    <string name=\"data_title_studios\">Studios</string>\n    <string name=\"season_winter\">Winter</string>\n    <string name=\"season_spring\">Spring</string>\n    <string name=\"season_summer\">Summer</string>\n    <string name=\"season_fall\">Fall</string>\n    <string name=\"relationship_sequel\">Sequel</string>\n    <string name=\"relationship_prequel\">Prequel</string>\n    <string name=\"relationship_alternative_setting\">Alt. setting</string>\n    <string name=\"relationship_alternative_version\">Alt. version</string>\n    <string name=\"relationship_side_story\">Side story</string>\n    <string name=\"relationship_parent_story\">Parent story</string>\n    <string name=\"relationship_summary\">Summary</string>\n    <string name=\"relationship_full_story\">Full story</string>\n    <string name=\"relationship_spinoff\">Spin-off</string>\n    <string name=\"relationship_adaptation\">Adaptation</string>\n    <string name=\"relationship_character\">Character</string>\n    <string name=\"relationship_other\">Other</string>\n    <string name=\"section_trending\">Trending This Week</string>\n    <string name=\"section_top_airing_anime\">Top Airing Anime</string>\n    <string name=\"section_top_upcoming_anime\">Top Upcoming Anime</string>\n    <string name=\"section_highest_rated_anime\">Highest Rated Anime</string>\n    <string name=\"section_most_popular_anime\">Most Popular Anime</string>\n    <string name=\"section_top_airing_manga\">Top Publishing Manga</string>\n    <string name=\"section_top_upcoming_manga\">Top Upcoming Manga</string>\n    <string name=\"section_highest_rated_manga\">Highest Rated Manga</string>\n    <string name=\"section_most_popular_manga\">Most Popular Manga</string>\n    <string name=\"preference_category_ui\">User Interface</string>\n    <string name=\"preference_category_account\">Account</string>\n    <string name=\"preference_category_advanced\">Advanced</string>\n    <string name=\"preference_category_about\">About</string>\n    <string name=\"preference_appearance_description\">Change the theme and toggle dark mode.</string>\n    <string name=\"preference_dark_mode\">Dark Mode</string>\n    <string-array name=\"preference_dark_mode_entries\" translatable=\"false\">\n        <item>@string/preference_dark_mode_light</item>\n        <item>@string/preference_dark_mode_dark</item>\n        <item>@string/preference_dark_mode_follow_system</item>\n        <item>@string/preference_dark_mode_battery_saver</item>\n    </string-array>\n    <string name=\"preference_dark_mode_light\">Light</string>\n    <string name=\"preference_dark_mode_dark\">Dark</string>\n    <string name=\"preference_dark_mode_follow_system\">Follow System</string>\n    <string name=\"preference_dark_mode_battery_saver\">Battery Saver</string>\n    <string name=\"preference_oled_black_mode\">OLED Black</string>\n    <string name=\"preference_oled_black_mode_description\">Use a black background in dark mode.</string>\n    <string name=\"preference_app_theme\">Theme</string>\n    <string name=\"preference_dynamic_color_theme\">Use Dynamic Color</string>\n    <string name=\"preference_dynamic_color_theme_description\">Adapt to the system theme.</string>\n    <string name=\"preference_app_theme_default\">Default</string>\n    <string name=\"preference_app_theme_purple\">Purple</string>\n    <string name=\"preference_app_theme_blue\">Blue</string>\n    <string name=\"preference_app_theme_green\">Green</string>\n    <string name=\"preference_media_item_size\">Poster Image Size</string>\n    <string-array name=\"preference_media_item_size_entries\" translatable=\"false\">\n        <item>@string/preference_media_item_size_small</item>\n        <item>@string/preference_media_item_size_medium</item>\n        <item>@string/preference_media_item_size_large</item>\n    </string-array>\n    <string name=\"preference_media_item_size_small\">Small</string>\n    <string name=\"preference_media_item_size_medium\">Medium</string>\n    <string name=\"preference_media_item_size_large\">Large</string>\n    <string name=\"preference_start_fragment\">Default Page</string>\n    <string-array name=\"preference_start_fragment_entries\" translatable=\"false\">\n        <item>@string/preference_start_fragment_home</item>\n        <item>@string/preference_start_fragment_search</item>\n        <item>@string/preference_start_fragment_library</item>\n        <item>@string/preference_start_fragment_profile</item>\n    </string-array>\n    <string name=\"preference_start_fragment_home\">Home</string>\n    <string name=\"preference_start_fragment_search\">Search</string>\n    <string name=\"preference_start_fragment_library\">Library</string>\n    <string name=\"preference_start_fragment_profile\">Profile</string>\n    <string name=\"preference_start_fragment_description\">The default page is set to %s.</string>\n    <string name=\"preference_language\">Language</string>\n    <string name=\"preference_language_default\">Default language</string>\n    <string name=\"preference_display_name\">Display Name</string>\n    <string name=\"preference_profile_url\">Profile URL</string>\n    <string name=\"preference_profile_url_not_set\">No profile URL set.</string>\n    <string name=\"preference_country\">Country</string>\n    <string name=\"preference_country_summary\">Country is set to %s</string>\n    <string name=\"preference_country_summary_non\">No country specified.</string>\n    <string name=\"preference_titles\">Titles</string>\n    <string name=\"preference_titles_description\">Define how media titles will be displayed.</string>\n    <string-array name=\"preference_titles_values\" translatable=\"false\">\n        <item>@string/preference_titles_canonical</item>\n        <item>@string/preference_titles_romanized</item>\n        <item>@string/preference_titles_english</item>\n    </string-array>\n    <string name=\"preference_titles_canonical\">Canonical</string>\n    <string name=\"preference_titles_romanized\">Romanized</string>\n    <string name=\"preference_titles_english\">English</string>\n    <string name=\"preference_adult_content\">Adult Content</string>\n    <string name=\"preference_adult_content_description_sfw\">Media with R18+ age rating will be hidden.</string>\n    <string name=\"preference_adult_content_description_sometimes\">Media with R18+ age rating will be visible, but posts tagged NSFW will be hidden in the global feed.</string>\n    <string name=\"preference_adult_content_description_everywhere\">Media with R18+ age rating will be visible everywhere.</string>\n    <string-array name=\"preference_sfw_filter_values\" translatable=\"false\">\n        <item>@string/preference_sfw_filter_hide_all</item>\n        <item>@string/preference_sfw_filter_limit_to_feed</item>\n        <item>@string/preference_sfw_filter_show_all</item>\n    </string-array>\n    <string name=\"preference_sfw_filter_hide_all\">Hide all Adult Content</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Limit to Following Feed</string>\n    <string name=\"preference_sfw_filter_show_all\">Adult Content Everywhere</string>\n    <string name=\"preference_rating_system\">Rating System</string>\n    <string-array name=\"preference_rating_system_values\" translatable=\"false\">\n        <item>@string/preference_rating_system_simple</item>\n        <item>@string/preference_rating_system_regular</item>\n        <item>@string/preference_rating_system_advanced</item>\n    </string-array>\n    <string name=\"preference_rating_system_simple\">Simple</string>\n    <string name=\"preference_rating_system_regular\">Regular</string>\n    <string name=\"preference_rating_system_advanced\">Advanced</string>\n    <string name=\"preference_remember_search_filters\">Remember Search Filters</string>\n    <string name=\"preference_remember_search_filters_description\">Apply the last used search filters after an app restart.</string>\n    <string name=\"preference_force_legacy_image_picker\">Force legacy Photo Picker</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Use the system file picker instead of the new photo picker.</string>\n    <string name=\"preference_check_for_updates\">Check for Updates on Launch</string>\n    <string name=\"preference_check_for_updates_description\">Check for a new version of the app on startup.</string>\n    <string name=\"preference_app_logs\" translatable=\"false\">@string/nav_app_logs</string>\n    <string name=\"preference_app_logs_description\">See and share the application log messages.</string>\n    <string name=\"preference_app_version\">Version Info</string>\n    <string name=\"preference_app_version_description\">Click to check for updates.</string>\n    <string name=\"preference_github_repo\">GitHub Repository</string>\n    <string name=\"preference_open_source_libraries\" translatable=\"false\">@string/nav_os_libraries</string>\n    <string name=\"preference_open_source_libraries_description\">A list of all used open source libraries.</string>\n    <string name=\"preference_not_logged_in\">You must be logged in to edit this setting.</string>\n    <string name=\"app_logs_no_data\">No logs available.</string>\n    <string name=\"prompt_email\">Email</string>\n    <string name=\"prompt_password\">Password</string>\n    <string name=\"action_sign_in\">Sign in</string>\n    <string name=\"action_log_in\">Log in</string>\n    <string name=\"action_log_in_to_kitsu\">Log in to Kitsu</string>\n    <string name=\"action_create_account\">Create account</string>\n    <string name=\"invalid_username\">Not a valid email address</string>\n    <string name=\"login_failed\">\"Login failed\"</string>\n    <string name=\"status_logging_in\">Logging in…</string>\n    <string name=\"logged_in_success\">Logged in as %s.</string>\n    <string name=\"login_no_account\">Don\\'t have an account? Create one on <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"not_logged_in\">Not logged in</string>\n    <string name=\"library_not_started\">Not Started</string>\n    <string name=\"library_not_rated\">Not Rated</string>\n    <string name=\"library_not_logged_in_title\">Log in to access your library</string>\n    <string name=\"library_not_logged_in_text\">Log in to your Kitsu Account to manage your library and get access to all features.</string>\n    <string name=\"library_kind_all\">All</string>\n    <string name=\"library_not_synchronized\">Not Synced</string>\n    <string name=\"library_edit_status\">Library Status</string>\n    <string name=\"library_edit_progress\">Progress</string>\n    <string name=\"library_edit_volumes\">Volumes</string>\n    <string name=\"library_edit_rating\">Rating</string>\n    <string name=\"library_edit_rewatch_count\">Rewatch Count</string>\n    <string name=\"library_edit_reread_count\">Reread Count</string>\n    <string name=\"library_edit_start_rewatch\">Start Rewatch</string>\n    <string name=\"library_edit_start_reread\">Start Reread</string>\n    <string name=\"library_edit_privacy\">Privacy</string>\n    <string name=\"library_edit_privacy_public\">Public</string>\n    <string name=\"library_edit_privacy_private\">Private</string>\n    <string name=\"library_edit_started\">Started</string>\n    <string name=\"library_edit_finished\">Finished</string>\n    <string name=\"library_edit_no_date_set\">No Date set</string>\n    <string name=\"library_edit_notes\">Personal Notes</string>\n    <string name=\"info_log_in_required\">Log in to access all features.</string>\n    <string name=\"info_image_saved_in_gallery\">Successfully saved image in gallery.</string>\n    <string name=\"info_update_checking_new_version\">Checking for a new version…</string>\n    <string name=\"info_update_new_version_available\">New version available.</string>\n    <string name=\"info_update_new_version_available_text\">Version %s of Kitsune is available.</string>\n    <string name=\"info_update_no_new_version_available\">No new version available.</string>\n    <string name=\"info_update_failed\">Failed to check for a new version.</string>\n    <string name=\"info_logged_out_token_expired\">Your access token has expired. Please log in again.</string>\n    <string name=\"filter_kind\">Kind</string>\n    <string name=\"filter_year\">Year</string>\n    <string name=\"filter_avg_rating\">Average Rating</string>\n    <string name=\"filter_select_categories\">Select Categories</string>\n    <string name=\"filter_season\">Season</string>\n    <string name=\"filter_subtype\">Subtype</string>\n    <string name=\"filter_streamers\">Streamers</string>\n    <string name=\"filter_age_rating\">Age Rating</string>\n    <string name=\"profile_not_logged_in_title\">Nothing to see here!</string>\n    <string name=\"profile_not_logged_in_text\">Log in to your Kitsu Account or create a new Account to get access to all features.</string>\n    <string name=\"profile_data_following\">%d Following</string>\n    <string name=\"profile_data_followers\">%d Followers</string>\n    <string name=\"profile_anime_stats\">Anime Stats</string>\n    <string name=\"profile_manga_stats\">Manga Stats</string>\n    <string name=\"profile_stats_anime_watch_time\">%s spent watching anime.</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d chapters read.</string>\n    <string name=\"profile_stats_time_spent_total\">%s in total.</string>\n    <string name=\"profile_stats_completed\">%1$d completed. More than &lt;b&gt;%2$d%%&lt;/b&gt; of users.</string>\n    <string name=\"profile_data_gender\">Gender</string>\n    <string name=\"profile_data_location\">Location</string>\n    <string name=\"profile_data_birthday\">Birthday</string>\n    <string name=\"profile_data_join_date\">Join Date</string>\n    <string name=\"profile_data_join_date_ago\">%s ago</string>\n    <string name=\"profile_data_private\">It\\'s a secret.</string>\n    <string name=\"profile_gender_male\">Male</string>\n    <string name=\"profile_gender_female\">Female</string>\n    <string name=\"profile_gender_custom\">Custom</string>\n    <string name=\"profile_gender_custom_hint\">Define your gender</string>\n    <string name=\"profile_data_waifu\">Waifu</string>\n    <string name=\"profile_data_husbando\">Husbando</string>\n    <string name=\"profile_waifu_or_husbando\">Waifu or Husbando</string>\n    <string name=\"profile_find_waifu\">Find your special someone</string>\n    <string name=\"profile_data_bio\">Bio</string>\n    <string name=\"profile_cover_image_description\">Cover image</string>\n    <string name=\"profile_avatar_image_description\">Profile picture</string>\n    <string name=\"profile_link_url\">URL or Username</string>\n    <string name=\"notification_updates\">Application Updates</string>\n\n    <string name=\"widget_description\">Library progress</string>\n    <string name=\"widget_empty_text\">There is no active tracked item in your library</string>\n    <string name=\"widget_empty_action\">Open Library</string>\n\n    <string name=\"onboarding_welcome_title\">Welcome to Kitsune!</string>\n    <string name=\"onboarding_welcome_subtitle\">Your personal anime and manga companion.</string>\n    <string name=\"onboarding_welcome_feature_search\">Search</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Find your favorite anime and manga.</string>\n    <string name=\"onboarding_welcome_feature_explore\">Explore</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Discover new and trending titles.</string>\n    <string name=\"onboarding_welcome_feature_track\">Track</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Keep track of what you\\'ve watched and read.</string>\n    <string name=\"onboarding_welcome_action\">Get Started</string>\n    <string name=\"onboarding_login_title\">Log in to Kitsu</string>\n    <string name=\"onboarding_login_subtitle\">Login to your Kitsu account or create a new account to get access to all features.</string>\n    <string name=\"onboarding_login_is_logged_in\">You are logged in</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Create Account</string>\n    <string name=\"onboarding_login_forward_dialog_message\">You will be redirected to %1$s where you can create a new account.</string>\n    <string name=\"onboarding_setup_title\">Initial Setup</string>\n    <string name=\"onboarding_setup_subtitle\">Set your preferred settings to get started. You can change them anytime later in the settings.</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">Notification permission is required.</string>\n    <string name=\"onboarding_setup_updates\">Check for Updates</string>\n    <string name=\"onboarding_setup_updates_description\">Get notified when a new release is available on GitHub.</string>\n    <string name=\"onboarding_setup_title_language\">Title Language</string>\n    <string name=\"onboarding_setup_title_language_description\">Select your preferred language for titles.</string>\n    <string name=\"onboarding_setup_title_language_selected\">Selected: %1$s</string>\n    <string name=\"onboarding_setup_action\">Finish Setup</string>\n\n    <string name=\"unit_episode\">Episode %d</string>\n    <string name=\"unit_chapter\">Chapter %d</string>\n    <!-- Unit Plurals -->\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s Page</item>\n        <item quantity=\"other\">%s Pages</item>\n    </plurals>\n    <!-- Duration Plurals -->\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s second</item>\n        <item quantity=\"other\">%s seconds</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s minute</item>\n        <item quantity=\"other\">%s minutes</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s hour</item>\n        <item quantity=\"other\">%s hours</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s day</item>\n        <item quantity=\"other\">%s days</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s month</item>\n        <item quantity=\"other\">%s months</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s year</item>\n        <item quantity=\"other\">%s years</item>\n    </plurals>\n    <!-- Transition names -->\n    <string name=\"unique_poster_transition_name\" translatable=\"false\">media_item_poster_%1$s</string>\n    <string name=\"details_poster_transition_name\" translatable=\"false\">media_details_poster</string>\n    <string name=\"character_picture_transition_name\" translatable=\"false\">character_picture</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"TextAppearance.Kitsune.SubtypeBadge\" parent=\"TextAppearance.Material3.LabelMedium\">\n        <item name=\"android:textColor\">@color/white</item>\n        <item name=\"android:textSize\">12sp</item>\n    </style>\n\n    <style name=\"Widget.Kitsune.TextView.BottomSheetTitle\" parent=\"Widget.AppCompat.TextView\">\n        <item name=\"android:textAppearance\">?attr/textAppearanceTitleMedium</item>\n        <item name=\"android:textSize\">18sp</item>\n        <item name=\"android:layout_marginBottom\">20dp</item>\n        <item name=\"android:maxLines\">2</item>\n        <item name=\"android:ellipsize\">end</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.Slide\">\n        <item name=\"android:windowEnterAnimation\">@anim/slide_up</item>\n        <item name=\"android:windowExitAnimation\">@anim/slide_down</item>\n    </style>\n\n    <style name=\"Widget.Kitsune.Button.IconButton.Dense\" parent=\"Widget.Material3.Button.IconButton\">\n        <item name=\"android:insetTop\">0dp</item>\n        <item name=\"android:insetBottom\">0dp</item>\n        <item name=\"android:insetLeft\">0dp</item>\n        <item name=\"android:insetRight\">0dp</item>\n        <item name=\"android:paddingLeft\">@dimen/icon_button_padding</item>\n        <item name=\"android:paddingRight\">@dimen/icon_button_padding</item>\n        <item name=\"android:paddingTop\">@dimen/icon_button_padding</item>\n        <item name=\"android:paddingBottom\">@dimen/icon_button_padding</item>\n    </style>\n\n    <style name=\"Widget.Kitsune.CardView.Surface\" parent=\"Widget.Material3.CardView.Outlined\">\n        <item name=\"strokeWidth\">0dp</item>\n    </style>\n\n    <!-- SearchView Style -->\n    <style name=\"Widget.Kitsune.SearchView\" parent=\"@style/Widget.AppCompat.SearchView\">\n        <item name=\"iconifiedByDefault\">false</item>\n        <item name=\"queryBackground\">@android:color/transparent</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.PreferenceThemeOverlay\" parent=\"@style/PreferenceThemeOverlay\">\n        <item name=\"android:layout\">@layout/fragment_preference</item>\n        <!-- use Material 3 switch -->\n        <item name=\"switchPreferenceCompatStyle\">@style/Preference.SwitchPreferenceCompat.Material3</item>\n    </style>\n\n    <style name=\"Preference.SwitchPreferenceCompat.Material3\" parent=\"@style/Preference.SwitchPreferenceCompat.Material\">\n        <item name=\"widgetLayout\">@layout/custom_preference_switch</item>\n    </style>\n\n    <style name=\"ShapeAppearance.Kitsune.LargeComponent\" parent=\"ShapeAppearance.Material3.LargeComponent\">\n        <item name=\"cornerFamily\">rounded</item>\n        <item name=\"cornerSize\">12dp</item>\n    </style>\n\n    <style name=\"ShapeAppearance.Kitsune.SubtypeBadge\" parent=\"\">\n        <item name=\"cornerFamily\">rounded</item>\n        <item name=\"cornerSizeBottomLeft\">4dp</item>\n        <item name=\"cornerSizeTopLeft\">0dp</item>\n        <item name=\"cornerSizeTopRight\">0dp</item>\n        <item name=\"cornerSizeBottomRight\">0dp</item>\n    </style>\n\n    <!-- Override default colors of the LinearProgressIndicator on devices with SDK version below 31 -->\n    <style name=\"Glance.AppWidget.LinearProgressIndicator\" parent=\"@style/Widget.AppCompat.ProgressBar.Horizontal\">\n        <item name=\"android:progressDrawable\">@drawable/progress_horizontal</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.Kitsune.DayNight\" parent=\"Theme.Kitsune\" />\n\n    <style name=\"Theme.Kitsune\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n        <item name=\"colorPrimary\">@color/md_theme_primary</item>\n        <item name=\"colorOnPrimary\">@color/md_theme_onPrimary</item>\n        <item name=\"colorPrimaryContainer\">@color/md_theme_primaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/md_theme_onPrimaryContainer</item>\n        <item name=\"colorSecondary\">@color/md_theme_secondary</item>\n        <item name=\"colorOnSecondary\">@color/md_theme_onSecondary</item>\n        <item name=\"colorSecondaryContainer\">@color/md_theme_secondaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/md_theme_onSecondaryContainer</item>\n        <item name=\"colorTertiary\">@color/md_theme_tertiary</item>\n        <item name=\"colorOnTertiary\">@color/md_theme_onTertiary</item>\n        <item name=\"colorTertiaryContainer\">@color/md_theme_tertiaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/md_theme_onTertiaryContainer</item>\n        <item name=\"colorError\">@color/md_theme_error</item>\n        <item name=\"colorOnError\">@color/md_theme_onError</item>\n        <item name=\"colorErrorContainer\">@color/md_theme_errorContainer</item>\n        <item name=\"colorOnErrorContainer\">@color/md_theme_onErrorContainer</item>\n        <item name=\"android:colorBackground\">@color/md_theme_background</item>\n        <item name=\"colorOnBackground\">@color/md_theme_onBackground</item>\n        <item name=\"colorSurface\">@color/md_theme_surface</item>\n        <item name=\"colorOnSurface\">@color/md_theme_onSurface</item>\n        <item name=\"colorSurfaceVariant\">@color/md_theme_surfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/md_theme_onSurfaceVariant</item>\n        <item name=\"colorOutline\">@color/md_theme_outline</item>\n        <item name=\"colorOutlineVariant\">@color/md_theme_outlineVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/md_theme_inverseSurface</item>\n        <item name=\"colorOnSurfaceInverse\">@color/md_theme_inverseOnSurface</item>\n        <item name=\"colorPrimaryInverse\">@color/md_theme_inversePrimary</item>\n        <item name=\"colorPrimaryFixed\">@color/md_theme_primaryFixed</item>\n        <item name=\"colorOnPrimaryFixed\">@color/md_theme_onPrimaryFixed</item>\n        <item name=\"colorPrimaryFixedDim\">@color/md_theme_primaryFixedDim</item>\n        <item name=\"colorOnPrimaryFixedVariant\">@color/md_theme_onPrimaryFixedVariant</item>\n        <item name=\"colorSecondaryFixed\">@color/md_theme_secondaryFixed</item>\n        <item name=\"colorOnSecondaryFixed\">@color/md_theme_onSecondaryFixed</item>\n        <item name=\"colorSecondaryFixedDim\">@color/md_theme_secondaryFixedDim</item>\n        <item name=\"colorOnSecondaryFixedVariant\">@color/md_theme_onSecondaryFixedVariant</item>\n        <item name=\"colorTertiaryFixed\">@color/md_theme_tertiaryFixed</item>\n        <item name=\"colorOnTertiaryFixed\">@color/md_theme_onTertiaryFixed</item>\n        <item name=\"colorTertiaryFixedDim\">@color/md_theme_tertiaryFixedDim</item>\n        <item name=\"colorOnTertiaryFixedVariant\">@color/md_theme_onTertiaryFixedVariant</item>\n        <item name=\"colorSurfaceDim\">@color/md_theme_surfaceDim</item>\n        <item name=\"colorSurfaceBright\">@color/md_theme_surfaceBright</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/md_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/md_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/md_theme_surfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_theme_surfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_theme_surfaceContainerHighest</item>\n\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n        <item name=\"android:windowLightStatusBar\">true</item>\n        <item name=\"android:windowLightNavigationBar\" tools:targetApi=\"o_mr1\">true</item>\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n\n        <item name=\"android:enforceStatusBarContrast\" tools:targetApi=\"q\">false</item>\n        <item name=\"android:enforceNavigationBarContrast\" tools:targetApi=\"q\">false</item>\n\n        <item name=\"colorPlaceholderGradientStart\">?colorPrimary</item>\n        <item name=\"colorPlaceholderGradientEnd\">?colorPrimaryDark</item>\n\n        <item name=\"preferenceTheme\">@style/Theme.Kitsune.PreferenceThemeOverlay</item>\n        <item name=\"bottomSheetDialogTheme\">@style/ThemeOverlay.Kitsune.BottomSheetDialog</item>\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Purple\" parent=\"Theme.Kitsune.DayNight\">\n        <item name=\"colorPrimary\">@color/md_purple_theme_primary</item>\n        <item name=\"colorOnPrimary\">@color/md_purple_theme_onPrimary</item>\n        <item name=\"colorPrimaryContainer\">@color/md_purple_theme_primaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/md_purple_theme_onPrimaryContainer</item>\n        <item name=\"colorSecondary\">@color/md_purple_theme_secondary</item>\n        <item name=\"colorOnSecondary\">@color/md_purple_theme_onSecondary</item>\n        <item name=\"colorSecondaryContainer\">@color/md_purple_theme_secondaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/md_purple_theme_onSecondaryContainer</item>\n        <item name=\"colorTertiary\">@color/md_purple_theme_tertiary</item>\n        <item name=\"colorOnTertiary\">@color/md_purple_theme_onTertiary</item>\n        <item name=\"colorTertiaryContainer\">@color/md_purple_theme_tertiaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/md_purple_theme_onTertiaryContainer</item>\n        <item name=\"colorError\">@color/md_purple_theme_error</item>\n        <item name=\"colorOnError\">@color/md_purple_theme_onError</item>\n        <item name=\"colorErrorContainer\">@color/md_purple_theme_errorContainer</item>\n        <item name=\"colorOnErrorContainer\">@color/md_purple_theme_onErrorContainer</item>\n        <item name=\"android:colorBackground\">@color/md_purple_theme_background</item>\n        <item name=\"colorOnBackground\">@color/md_purple_theme_onBackground</item>\n        <item name=\"colorSurface\">@color/md_purple_theme_surface</item>\n        <item name=\"colorOnSurface\">@color/md_purple_theme_onSurface</item>\n        <item name=\"colorSurfaceVariant\">@color/md_purple_theme_surfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/md_purple_theme_onSurfaceVariant</item>\n        <item name=\"colorOutline\">@color/md_purple_theme_outline</item>\n        <item name=\"colorOutlineVariant\">@color/md_purple_theme_outlineVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/md_purple_theme_inverseSurface</item>\n        <item name=\"colorOnSurfaceInverse\">@color/md_purple_theme_inverseOnSurface</item>\n        <item name=\"colorPrimaryInverse\">@color/md_purple_theme_inversePrimary</item>\n        <item name=\"colorPrimaryFixed\">@color/md_purple_theme_primaryFixed</item>\n        <item name=\"colorOnPrimaryFixed\">@color/md_purple_theme_onPrimaryFixed</item>\n        <item name=\"colorPrimaryFixedDim\">@color/md_purple_theme_primaryFixedDim</item>\n        <item name=\"colorOnPrimaryFixedVariant\">@color/md_purple_theme_onPrimaryFixedVariant</item>\n        <item name=\"colorSecondaryFixed\">@color/md_purple_theme_secondaryFixed</item>\n        <item name=\"colorOnSecondaryFixed\">@color/md_purple_theme_onSecondaryFixed</item>\n        <item name=\"colorSecondaryFixedDim\">@color/md_purple_theme_secondaryFixedDim</item>\n        <item name=\"colorOnSecondaryFixedVariant\">@color/md_purple_theme_onSecondaryFixedVariant</item>\n        <item name=\"colorTertiaryFixed\">@color/md_purple_theme_tertiaryFixed</item>\n        <item name=\"colorOnTertiaryFixed\">@color/md_purple_theme_onTertiaryFixed</item>\n        <item name=\"colorTertiaryFixedDim\">@color/md_purple_theme_tertiaryFixedDim</item>\n        <item name=\"colorOnTertiaryFixedVariant\">@color/md_purple_theme_onTertiaryFixedVariant</item>\n        <item name=\"colorSurfaceDim\">@color/md_purple_theme_surfaceDim</item>\n        <item name=\"colorSurfaceBright\">@color/md_purple_theme_surfaceBright</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/md_purple_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/md_purple_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/md_purple_theme_surfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_purple_theme_surfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_purple_theme_surfaceContainerHighest</item>\n\n        <item name=\"colorPlaceholderGradientStart\">?colorPrimary</item>\n        <item name=\"colorPlaceholderGradientEnd\">?colorPrimaryVariant</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Purple</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Blue\" parent=\"Theme.Kitsune.DayNight\">\n        <item name=\"colorPrimary\">@color/md_blue_theme_primary</item>\n        <item name=\"colorOnPrimary\">@color/md_blue_theme_onPrimary</item>\n        <item name=\"colorPrimaryContainer\">@color/md_blue_theme_primaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/md_blue_theme_onPrimaryContainer</item>\n        <item name=\"colorSecondary\">@color/md_blue_theme_secondary</item>\n        <item name=\"colorOnSecondary\">@color/md_blue_theme_onSecondary</item>\n        <item name=\"colorSecondaryContainer\">@color/md_blue_theme_secondaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/md_blue_theme_onSecondaryContainer</item>\n        <item name=\"colorTertiary\">@color/md_blue_theme_tertiary</item>\n        <item name=\"colorOnTertiary\">@color/md_blue_theme_onTertiary</item>\n        <item name=\"colorTertiaryContainer\">@color/md_blue_theme_tertiaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/md_blue_theme_onTertiaryContainer</item>\n        <item name=\"colorError\">@color/md_blue_theme_error</item>\n        <item name=\"colorOnError\">@color/md_blue_theme_onError</item>\n        <item name=\"colorErrorContainer\">@color/md_blue_theme_errorContainer</item>\n        <item name=\"colorOnErrorContainer\">@color/md_blue_theme_onErrorContainer</item>\n        <item name=\"android:colorBackground\">@color/md_blue_theme_background</item>\n        <item name=\"colorOnBackground\">@color/md_blue_theme_onBackground</item>\n        <item name=\"colorSurface\">@color/md_blue_theme_surface</item>\n        <item name=\"colorOnSurface\">@color/md_blue_theme_onSurface</item>\n        <item name=\"colorSurfaceVariant\">@color/md_blue_theme_surfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/md_blue_theme_onSurfaceVariant</item>\n        <item name=\"colorOutline\">@color/md_blue_theme_outline</item>\n        <item name=\"colorOutlineVariant\">@color/md_blue_theme_outlineVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/md_blue_theme_inverseSurface</item>\n        <item name=\"colorOnSurfaceInverse\">@color/md_blue_theme_inverseOnSurface</item>\n        <item name=\"colorPrimaryInverse\">@color/md_blue_theme_inversePrimary</item>\n        <item name=\"colorPrimaryFixed\">@color/md_blue_theme_primaryFixed</item>\n        <item name=\"colorOnPrimaryFixed\">@color/md_blue_theme_onPrimaryFixed</item>\n        <item name=\"colorPrimaryFixedDim\">@color/md_blue_theme_primaryFixedDim</item>\n        <item name=\"colorOnPrimaryFixedVariant\">@color/md_blue_theme_onPrimaryFixedVariant</item>\n        <item name=\"colorSecondaryFixed\">@color/md_blue_theme_secondaryFixed</item>\n        <item name=\"colorOnSecondaryFixed\">@color/md_blue_theme_onSecondaryFixed</item>\n        <item name=\"colorSecondaryFixedDim\">@color/md_blue_theme_secondaryFixedDim</item>\n        <item name=\"colorOnSecondaryFixedVariant\">@color/md_blue_theme_onSecondaryFixedVariant</item>\n        <item name=\"colorTertiaryFixed\">@color/md_blue_theme_tertiaryFixed</item>\n        <item name=\"colorOnTertiaryFixed\">@color/md_blue_theme_onTertiaryFixed</item>\n        <item name=\"colorTertiaryFixedDim\">@color/md_blue_theme_tertiaryFixedDim</item>\n        <item name=\"colorOnTertiaryFixedVariant\">@color/md_blue_theme_onTertiaryFixedVariant</item>\n        <item name=\"colorSurfaceDim\">@color/md_blue_theme_surfaceDim</item>\n        <item name=\"colorSurfaceBright\">@color/md_blue_theme_surfaceBright</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/md_blue_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/md_blue_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/md_blue_theme_surfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_blue_theme_surfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_blue_theme_surfaceContainerHighest</item>\n\n        <item name=\"colorPlaceholderGradientStart\">?colorPrimary</item>\n        <item name=\"colorPlaceholderGradientEnd\">?colorPrimaryVariant</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Blue</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Green\" parent=\"Theme.Kitsune.DayNight\">\n        <item name=\"colorPrimary\">@color/md_green_theme_primary</item>\n        <item name=\"colorOnPrimary\">@color/md_green_theme_onPrimary</item>\n        <item name=\"colorPrimaryContainer\">@color/md_green_theme_primaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/md_green_theme_onPrimaryContainer</item>\n        <item name=\"colorSecondary\">@color/md_green_theme_secondary</item>\n        <item name=\"colorOnSecondary\">@color/md_green_theme_onSecondary</item>\n        <item name=\"colorSecondaryContainer\">@color/md_green_theme_secondaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/md_green_theme_onSecondaryContainer</item>\n        <item name=\"colorTertiary\">@color/md_green_theme_tertiary</item>\n        <item name=\"colorOnTertiary\">@color/md_green_theme_onTertiary</item>\n        <item name=\"colorTertiaryContainer\">@color/md_green_theme_tertiaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/md_green_theme_onTertiaryContainer</item>\n        <item name=\"colorError\">@color/md_green_theme_error</item>\n        <item name=\"colorOnError\">@color/md_green_theme_onError</item>\n        <item name=\"colorErrorContainer\">@color/md_green_theme_errorContainer</item>\n        <item name=\"colorOnErrorContainer\">@color/md_green_theme_onErrorContainer</item>\n        <item name=\"android:colorBackground\">@color/md_green_theme_background</item>\n        <item name=\"colorOnBackground\">@color/md_green_theme_onBackground</item>\n        <item name=\"colorSurface\">@color/md_green_theme_surface</item>\n        <item name=\"colorOnSurface\">@color/md_green_theme_onSurface</item>\n        <item name=\"colorSurfaceVariant\">@color/md_green_theme_surfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/md_green_theme_onSurfaceVariant</item>\n        <item name=\"colorOutline\">@color/md_green_theme_outline</item>\n        <item name=\"colorOutlineVariant\">@color/md_green_theme_outlineVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/md_green_theme_inverseSurface</item>\n        <item name=\"colorOnSurfaceInverse\">@color/md_green_theme_inverseOnSurface</item>\n        <item name=\"colorPrimaryInverse\">@color/md_green_theme_inversePrimary</item>\n        <item name=\"colorPrimaryFixed\">@color/md_green_theme_primaryFixed</item>\n        <item name=\"colorOnPrimaryFixed\">@color/md_green_theme_onPrimaryFixed</item>\n        <item name=\"colorPrimaryFixedDim\">@color/md_green_theme_primaryFixedDim</item>\n        <item name=\"colorOnPrimaryFixedVariant\">@color/md_green_theme_onPrimaryFixedVariant</item>\n        <item name=\"colorSecondaryFixed\">@color/md_green_theme_secondaryFixed</item>\n        <item name=\"colorOnSecondaryFixed\">@color/md_green_theme_onSecondaryFixed</item>\n        <item name=\"colorSecondaryFixedDim\">@color/md_green_theme_secondaryFixedDim</item>\n        <item name=\"colorOnSecondaryFixedVariant\">@color/md_green_theme_onSecondaryFixedVariant</item>\n        <item name=\"colorTertiaryFixed\">@color/md_green_theme_tertiaryFixed</item>\n        <item name=\"colorOnTertiaryFixed\">@color/md_green_theme_onTertiaryFixed</item>\n        <item name=\"colorTertiaryFixedDim\">@color/md_green_theme_tertiaryFixedDim</item>\n        <item name=\"colorOnTertiaryFixedVariant\">@color/md_green_theme_onTertiaryFixedVariant</item>\n        <item name=\"colorSurfaceDim\">@color/md_green_theme_surfaceDim</item>\n        <item name=\"colorSurfaceBright\">@color/md_green_theme_surfaceBright</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/md_green_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/md_green_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/md_green_theme_surfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_green_theme_surfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_green_theme_surfaceContainerHighest</item>\n\n        <item name=\"colorPlaceholderGradientStart\">?colorPrimary</item>\n        <item name=\"colorPlaceholderGradientEnd\">?colorPrimaryVariant</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Green</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Black\">\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Black</item>\n        <!-- effects only night mode -->\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Purple.Black\">\n        <!-- effects only night mode -->\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Blue.Black\">\n        <!-- effects only night mode -->\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Green.Black\">\n        <!-- effects only night mode -->\n    </style>\n\n    <!-- FullScreen theme -->\n    <style name=\"Theme.Kitsune.DayNight.FullScreen\">\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n        <item name=\"android:windowLightStatusBar\">false</item>\n        <item name=\"android:windowLightNavigationBar\" tools:targetApi=\"o_mr1\">false</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:targetApi=\"o_mr1\">shortEdges</item>\n\n        <item name=\"colorSurface\">@color/black</item>\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:colorBackgroundCacheHint\">@null</item>\n        <item name=\"android:windowContentOverlay\">@null</item>\n        <item name=\"android:windowIsFloating\">false</item>\n        <item name=\"android:windowIsTranslucent\">true</item>\n        <item name=\"android:windowNoTitle\">true</item>\n    </style>\n\n    <!-- SplashScreen theme -->\n    <style name=\"Theme.Kitsune.SplashScreen.DayNight\" parent=\"Theme.Kitsune.SplashScreen\" />\n\n    <style name=\"Theme.Kitsune.SplashScreen\" parent=\"Theme.SplashScreen\">\n        <item name=\"windowSplashScreenBackground\">@color/white</item>\n        <item name=\"windowSplashScreenAnimatedIcon\">@drawable/ic_splashscreen</item>\n        <item name=\"android:windowLightStatusBar\">true</item>\n        <item name=\"android:windowLightNavigationBar\" tools:targetApi=\"o_mr1\">true</item>\n        <item name=\"postSplashScreenTheme\">@style/Theme.Kitsune.DayNight</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog.Dynamic\" parent=\"ThemeOverlay.Material3.DynamicColors.DayNight\">\n        <item name=\"android:windowIsFloating\">false</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog\" parent=\"Theme.Kitsune.DayNight\">\n        <item name=\"android:windowIsFloating\">false</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog.Black\" parent=\"Theme.Kitsune.DayNight.Black\">\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog.Purple\" parent=\"Theme.Kitsune.DayNight.Purple\">\n        <item name=\"android:windowIsFloating\">false</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog.Purple.Black\" parent=\"Theme.Kitsune.DayNight.Purple.Black\">\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog.Blue\" parent=\"Theme.Kitsune.DayNight.Blue\">\n        <item name=\"android:windowIsFloating\">false</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog.Blue.Black\" parent=\"Theme.Kitsune.DayNight.Blue.Black\">\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog.Green\" parent=\"Theme.Kitsune.DayNight.Green\">\n        <item name=\"android:windowIsFloating\">false</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.FullScreenDialog.Green.Black\" parent=\"Theme.Kitsune.DayNight.Green.Black\">\n    </style>\n\n    <style name=\"ThemeOverlay.Kitsune.BottomSheetDialog\" parent=\"ThemeOverlay.Material3.BottomSheetDialog\">\n        <item name=\"android:windowIsFloating\">false</item>\n        <item name=\"bottomSheetStyle\">@style/ModalBottomSheet</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n        <item name=\"paddingLeftSystemWindowInsets\">false</item>\n        <item name=\"paddingRightSystemWindowInsets\">false</item>\n    </style>\n\n    <style name=\"ModalBottomSheet\" parent=\"Widget.Material3.BottomSheet.Modal\">\n        <item name=\"shapeAppearance\">@style/ShapeAppearance.Kitsune.LargeComponent</item>\n    </style>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values-af/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"nav_home\">Tuis</string>\n    <string name=\"nav_search\">Soek</string>\n    <string name=\"nav_library\">Biblioteek</string>\n    <string name=\"nav_profile\">Profiel</string>\n    <string name=\"nav_settings\">Instellings</string>\n    <string name=\"nav_appearance\">Tema</string>\n    <string name=\"nav_app_logs\">Applikasie Logs</string>\n    <string name=\"nav_os_libraries\">Oopbronbiblioteke</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"manga\">Manga</string>\n    <string name=\"action_retry\">Herprobeer</string>\n    <string name=\"action_ok\">OK</string>\n    <string name=\"action_cancel\">Kanselleer</string>\n    <string name=\"action_reset_filter\">Herstel filter</string>\n    <string name=\"action_unselect_all\">Deselecteer alles</string>\n    <string name=\"action_read_more\">Lees meer</string>\n    <string name=\"action_read_less\">Lees minder</string>\n    <string name=\"action_show_more\">Wys meer</string>\n    <string name=\"action_show_less\">Wys minder</string>\n    <string name=\"action_view\">Bekyk</string>\n    <string name=\"action_back\">Terug</string>\n    <string name=\"action_skip\">Oorslaan</string>\n    <string name=\"action_next\">Volgende</string>\n    <string name=\"action_continue\">Gaan voort</string>\n    <string name=\"action_close\">Maak toe</string>\n    <string name=\"action_log_out\">Teken uit</string>\n    <string name=\"action_share_profile_url\">Deel Profiel</string>\n    <string name=\"action_share\">Deel</string>\n    <string name=\"action_open_external\">Maak oop op \\'n eksterne webwerf</string>\n    <string name=\"action_open_external_short\">Eksterne webwerf</string>\n    <string name=\"action_share_app_logs\">Deel Logs</string>\n    <string name=\"action_add_to_favorites\">Voeg by gunstelinge</string>\n    <string name=\"action_remove_from_favorites\">Verwyder uit gunstelinge</string>\n    <string name=\"action_dismiss\">Ontslaan</string>\n    <string name=\"action_synchronize\">Sinkroniseer</string>\n    <string name=\"action_save_in_gallery\">Stoor na gallery</string>\n    <string name=\"action_open_in_browser\">Maak oop in Browser</string>\n    <string name=\"action_open_on_mal\">Maak oop op MyAnimeList.net</string>\n    <string name=\"action_save_changes\">Stoor Veranderinge</string>\n    <string name=\"action_edit_profile\">Wysig Profiel</string>\n    <string name=\"action_update_profile\">Opdateer Profiel</string>\n    <string name=\"action_add\">Voeg by</string>\n    <string name=\"action_remove\">Verwyder</string>\n    <string name=\"action_update\">Opdateer</string>\n    <string name=\"action_start\">Begin</string>\n    <string name=\"action_allow\">Laat toe</string>\n    <string name=\"action_rate\">Beoordeel</string>\n    <string name=\"action_update_rating\">Opdateer Beoordelling</string>\n    <string name=\"action_remove_rating\">Verwyder Beoordelling</string>\n    <string name=\"action_add_profile_link\">Voeg by Profielskakel</string>\n    <string name=\"action_edit_profile_link\">Wysig Profielskakel</string>\n    <string name=\"action_select_profile_link_site\">Kies n Webwerf</string>\n    <string name=\"action_delete_profile_link\">Verwyder Profielskakel</string>\n    <string name=\"hint_search\">Soek</string>\n    <string name=\"hint_search_characters\">Soek karakters</string>\n    <string name=\"hint_mark_watched\">Merk as gesien.</string>\n    <string name=\"hint_mark_not_watched\">Merk as nie gesien.</string>\n    <string name=\"hint_rating\">Beoordelling</string>\n    <string name=\"dialog_log_out_confirmation\">Is jy seker jy wil uitteken?</string>\n    <string name=\"dialog_remove_from_library_title\">Verwyder van Biblioteek?</string>\n    <string name=\"dialog_remove_from_library_msg\"><b>%s</b> sal van jou Biblioteek af verwyder word.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Permissie ontken</string>\n    <string name=\"dialog_notification_permission_rejected\">Jy sal nie notifikasies kry nie en sal nie ingelig word oor app opdaterings nie.</string>\n    <string name=\"dialog_request_notification_permission_title\">Laat notifikasies toe</string>\n    <string name=\"dialog_request_notification_permission\">Laat druknotifikasies toe om in kennis gestel te word wanneer ’n nuwe app weergawe vrygestel word.</string>\n    <string name=\"error_resource_loading\">Kon nie data laai nie.</string>\n    <string name=\"error_image_loading\">Kon nie foto oplaai nie.</string>\n    <string name=\"error_mapping_loading\">Kon nie kartering vir eksterne webwerwe laai nie.</string>\n    <string name=\"error_nothing_found\">Niks gevind nie.</string>\n    <string name=\"error_invalid_user\">Ongeldige Gebruiker. Is jy aangemeld?</string>\n    <string name=\"error_user_update_failed\">Kon nie gebruiker opdateer nie.</string>\n    <string name=\"error_user_delete_waifu_failed\">Kon nie Waifu/Husbando verwyder nie.</string>\n    <string name=\"error_user_update_image_failed\">Kon nie profiek foto of agtergrondfoto opdateer nie.</string>\n    <string name=\"error_user_create_profile_link_failed\">Kon nie profielskakel maak vir %s.</string>\n    <string name=\"error_user_update_profile_link_failed\">Kon nie profielskakel vir %s opdateer nie.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Kon nie profielskakel vir %s verwyder nie.</string>\n    <string name=\"error_something_wrong\">Oeps, iets het verkeerd gegaan!</string>\n    <string name=\"error_requires_external_storage_permission\">Vereis toestemming om na eksterne berging te skryf.</string>\n    <string name=\"error_search_provider_unavailable\">Soekverskaffer nie beskikbaar nie.</string>\n    <string name=\"error_library_update_failed\">Kon nie biblioteekinskrywing opdateer nie.</string>\n    <string name=\"error_library_update_failed_multiple\">Kon nie %d biblioteekinskrywings opdateer nie.</string>\n    <string name=\"error_library_update_not_found\">Die biblioteekinskrywing bestaan nie.</string>\n    <string name=\"error_library_add_failed\">Kon nie by jou biblioteek voeg nie.</string>\n    <string name=\"error_library_delete_failed\">Kon nie biblioteekinskrywing verwyder nie.</string>\n    <string name=\"error_requires_notification_permission\">Kennisgewingtoestemming word vereis.</string>\n    <string name=\"sort_popularity_asc\">Minste Gewild</string>\n    <string name=\"sort_popularity_desc\">Meeste Gewild</string>\n    <string name=\"sort_average_rating_asc\">Slegste Gegradeer</string>\n    <string name=\"sort_average_rating_desc\">Beste Gegradeer</string>\n    <string name=\"sort_release_date_asc\">Oudste</string>\n    <string name=\"sort_release_date_desc\">Nuutste</string>\n    <string name=\"title_media_type\">Media-tipe</string>\n    <string name=\"title_character_appearances\">Karakterverskynings</string>\n    <string name=\"title_categories\">Kategorieë</string>\n    <string name=\"title_filter\">Filtreer</string>\n    <string name=\"title_description\">Deskripsie</string>\n    <string name=\"title_details\">Besonderhede</string>\n    <string name=\"title_streamer\">Streaming-links</string>\n    <string name=\"title_ratings\">Gradeerings</string>\n    <string name=\"title_more_franchise\">Meer van hierdie Reeks</string>\n    <string name=\"title_favorite_anime\">Gunsteling Anime</string>\n    <string name=\"title_favorite_manga\">Gunsteling Manga</string>\n    <string name=\"title_favorite_characters\">Gunsteling Karakters</string>\n    <string name=\"title_authentication\">Verifikasie</string>\n    <string name=\"title_login\">Teken in op jou Kitsu-rekening</string>\n    <string name=\"title_play_trailer\">Speel Voorfilmpie</string>\n    <string name=\"title_about_me\">Oor My</string>\n    <string name=\"title_episodes\">Afleverings</string>\n    <string name=\"title_chapters\">Hoofstukke</string>\n    <string name=\"title_characters\">Karakters</string>\n    <string name=\"title_edit_library_entry\">Wysig Biblioteekinskrywing</string>\n    <string name=\"title_edit_profile\">Wysig Profiel</string>\n    <string name=\"no_information\">Onbekend</string>\n    <string name=\"no_data_available\">Geen Data Beskikbaar</string>\n    <string name=\"status_current\">Tans Uitgesaai</string>\n    <string name=\"status_current_manga\">Publiseer</string>\n    <string name=\"status_finished\">Klaar</string>\n    <string name=\"status_unreleased\">Nog nie gepubliseer nie</string>\n    <string name=\"status_upcoming\">Opkomend</string>\n    <string name=\"character_role_main\">Hoof</string>\n    <string name=\"character_role_supporting\">Byspel</string>\n    <string name=\"character_role_recurring\">Terugkerend</string>\n    <string name=\"character_role_cameo\">Kort verskyning</string>\n    <string name=\"library_status_watching\">Besig om te kyk</string>\n    <string name=\"library_status_planned\">Wil nog Kyk</string>\n    <string name=\"library_status_completed\">Klaar gekyk</string>\n    <string name=\"library_status_on_hold\">Gehou</string>\n    <string name=\"library_status_dropped\">Gelaat</string>\n    <string name=\"library_status_reading\">Besig om te lees</string>\n    <string name=\"library_status_planned_manga\">Wil nog Lees</string>\n    <string name=\"library_action_remove\">Verwyder van Biblioteek</string>\n    <string name=\"library_action_add\">Voeg by Biblioteek</string>\n    <string name=\"library_action_edit\">Wysig Biblioteekinskrywing</string>\n    <string name=\"data_rank\">Rang #%d</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"nav_app_logs\">Anwendungsprotokolle</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"sort_release_date_desc\">Am neusten</string>\n    <string name=\"title_filter\">Filter</string>\n    <string name=\"title_categories\">Kategorien</string>\n    <string name=\"title_description\">Beschreibung</string>\n    <string name=\"action_read_less\">Weniger anzeigen</string>\n    <string name=\"action_continue\">Fortfahren</string>\n    <string name=\"action_close\">Schließen</string>\n    <string name=\"action_open_external\">Auf externer Seite öffnen</string>\n    <string name=\"action_remove\">Entfernen</string>\n    <string name=\"action_update\">Aktualisieren</string>\n    <string name=\"action_start\">Starten</string>\n    <string name=\"action_allow\">Erlauben</string>\n    <string name=\"action_rate\">Bewerten</string>\n    <string name=\"action_update_rating\">Bewertung aktualisieren</string>\n    <string name=\"action_remove_rating\">Bewertung entfernen</string>\n    <string name=\"action_add_profile_link\">Profil-Link hinzufügen</string>\n    <string name=\"action_edit_profile_link\">Profil-Link bearbeiten</string>\n    <string name=\"action_select_profile_link_site\">Seite auswählen</string>\n    <string name=\"action_delete_profile_link\">Profil-Link löschen</string>\n    <string name=\"hint_search\">Suchen</string>\n    <string name=\"error_user_delete_profile_link_failed\">Profil-Link für %s konnte nicht gelöscht werden.</string>\n    <string name=\"error_requires_external_storage_permission\">Berechtigung zum Schreiben auf externen Speicher wird benötigt.</string>\n    <string name=\"title_play_trailer\">Trailer abspielen</string>\n    <string name=\"title_edit_profile\">Profil bearbeiten</string>\n    <string name=\"library_status_dropped\">Abgebrochen</string>\n    <string name=\"library_status_planned_manga\">Gemerkt</string>\n    <string name=\"library_action_remove\">Von Bibliothek entfernen</string>\n    <string name=\"library_action_add\">Zur Bibliothek hinzufügen</string>\n    <string name=\"library_action_edit\">Bibliothekseintrag bearbeiten</string>\n    <string name=\"data_rank\">Rang #%d</string>\n    <string name=\"data_title_published\">Veröffentlicht</string>\n    <string name=\"data_title_volumes\">Bände</string>\n    <string name=\"data_title_episodes\">Episoden</string>\n    <string name=\"data_title_licensors\">Lizenzgeber</string>\n    <string name=\"data_title_studios\">Studios</string>\n    <string name=\"data_title_length\">Länge</string>\n    <string name=\"data_title_producers\">Produzenten</string>\n    <string name=\"section_trending\">Diese Woche beliebt</string>\n    <string name=\"section_top_airing_anime\">Top laufende Anime</string>\n    <string name=\"sort_popularity_desc\">Am beliebtesten</string>\n    <string name=\"section_top_upcoming_manga\">Top bevorstehende Manga</string>\n    <string name=\"section_highest_rated_manga\">Bestbewertete Manga</string>\n    <string name=\"preference_category_ui\">Benutzeroberfläche</string>\n    <string name=\"preference_category_about\">Über</string>\n    <string name=\"preference_appearance_description\">Aussehen und Dunkler Modus ändern.</string>\n    <string name=\"preference_dark_mode_light\">Hell</string>\n    <string name=\"preference_oled_black_mode\">OLED Schwarz</string>\n    <string name=\"preference_oled_black_mode_description\">Nutze einen schwarzen Hintergrund im Dunklen Modus.</string>\n    <string name=\"preference_app_theme\">Thema</string>\n    <string name=\"preference_dynamic_color_theme_description\">An das Systemthema anpassen.</string>\n    <string name=\"preference_app_theme_green\">Grün</string>\n    <string name=\"preference_start_fragment\">Standardseite</string>\n    <string name=\"preference_start_fragment_search\">Suchen</string>\n    <string name=\"preference_start_fragment_library\">Bibliothek</string>\n    <string name=\"preference_start_fragment_profile\">Profil</string>\n    <string name=\"preference_country_summary_non\">Kein Land festgelegt.</string>\n    <string name=\"preference_titles_description\">Lege fest, wie Medientitel angezeigt werden.</string>\n    <string name=\"preference_titles_romanized\">Romanisiert</string>\n    <string name=\"preference_titles_english\">Englisch</string>\n    <string name=\"preference_adult_content_description_sometimes\">Medien mit R18+ Altersbeschränkung sind sichtbar, aber Beiträge mit NSFW Kennzeichnung werden im globalen Feed versteckt.</string>\n    <string name=\"preference_app_version_description\">Klicken, um nach Aktualisierungen zu suchen.</string>\n    <string name=\"action_log_in_to_kitsu\">Bei Kitsu anmelden</string>\n    <string name=\"action_create_account\">Account erstellen</string>\n    <string name=\"invalid_username\">Keine gültige E-Mail Adresse</string>\n    <string name=\"not_logged_in\">Nicht angemeldet</string>\n    <string name=\"app_logs_no_data\">Keine Protokolle verfügbar.</string>\n    <string name=\"prompt_email\">E-Mail</string>\n    <string name=\"library_edit_privacy_private\">Privat</string>\n    <string name=\"library_edit_finished\">Abgeschlossen</string>\n    <string name=\"library_edit_no_date_set\">Kein Datum festgelegt</string>\n    <string name=\"info_image_saved_in_gallery\">Bild wurde erfolgreich in der Galerie gespeichert.</string>\n    <string name=\"info_update_failed\">Konnte nicht auf neue Version überprüfen.</string>\n    <string name=\"info_logged_out_token_expired\">Dein Zugriffstoken ist abgelaufen. Bitte melde dich erneut an.</string>\n    <string name=\"filter_kind\">Art</string>\n    <string name=\"filter_year\">Jahr</string>\n    <string name=\"filter_avg_rating\">Durchschnittliche Bewertung</string>\n    <string name=\"filter_select_categories\">Kategorien auswählen</string>\n    <string name=\"filter_season\">Season</string>\n    <string name=\"filter_subtype\">Untertyp</string>\n    <string name=\"filter_streamers\">Streaminganbieter</string>\n    <string name=\"profile_not_logged_in_title\">Hier gibt\\'s nicht zu sehen!</string>\n    <string name=\"filter_age_rating\">Altersfreigabe</string>\n    <string name=\"nav_appearance\">Aussehen</string>\n    <string name=\"nav_os_libraries\">Open Source Bibliotheken</string>\n    <string name=\"manga\">Manga</string>\n    <string name=\"action_retry\">Erneut versuchen</string>\n    <string name=\"action_ok\">Ok</string>\n    <string name=\"action_cancel\">Abbrechen</string>\n    <string name=\"action_reset_filter\">Filter zurücksetzen</string>\n    <string name=\"action_unselect_all\">Alle abwählen</string>\n    <string name=\"action_read_more\">Mehr anzeigen</string>\n    <string name=\"action_show_more\">Mehr anzeigen</string>\n    <string name=\"action_show_less\">Weniger anzeigen</string>\n    <string name=\"action_view\">Anzeigen</string>\n    <string name=\"action_back\">Zurück</string>\n    <string name=\"action_log_out\">Abmelden</string>\n    <string name=\"action_share_profile_url\">Profil teilen</string>\n    <string name=\"action_share\">Teilen</string>\n    <string name=\"action_open_external_short\">Externe Seiten</string>\n    <string name=\"action_share_app_logs\">Protokolle teilen</string>\n    <string name=\"action_add_to_favorites\">Zu Favoriten hinzufügen</string>\n    <string name=\"action_remove_from_favorites\">Von Favoriten entfernen</string>\n    <string name=\"action_dismiss\">Verwerfen</string>\n    <string name=\"action_synchronize\">Synchronisieren</string>\n    <string name=\"action_save_in_gallery\">In Galerie speichern</string>\n    <string name=\"action_open_in_browser\">Im Browser öffnen</string>\n    <string name=\"action_open_on_mal\">Auf MyAnimeList.net öffnen</string>\n    <string name=\"action_save_changes\">Änderungen speichern</string>\n    <string name=\"action_edit_profile\">Profil bearbeiten</string>\n    <string name=\"action_update_profile\">Profil aktualisieren</string>\n    <string name=\"action_add\">Hinzufügen</string>\n    <string name=\"hint_search_characters\">Charaktere suchen</string>\n    <string name=\"hint_mark_watched\">Als gesehen markieren.</string>\n    <string name=\"hint_mark_not_watched\">Als nicht gesehen markieren.</string>\n    <string name=\"hint_rating\">Bewertung</string>\n    <string name=\"dialog_log_out_confirmation\">Bist du sicher, dass du dich abmelden möchtest?</string>\n    <string name=\"dialog_remove_from_library_title\">Von Bibliothek entfernen?</string>\n    <string name=\"dialog_remove_from_library_msg\">&lt;b&gt;%s&lt;/b&gt; wird von deiner Bibliothek entfernt.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Berechtigung abgelehnt</string>\n    <string name=\"dialog_notification_permission_rejected\">Du wirst keine Benachrichtigungen erhalten und wirst nicht über App-Aktualisierungen informiert.</string>\n    <string name=\"dialog_request_notification_permission_title\">Benachrichtigungen erlauben</string>\n    <string name=\"dialog_request_notification_permission\">Erlauben Push-Benachrichtigungen, um über neu veröffentlichte App-Versionen benachrichtigt zu werden.</string>\n    <string name=\"error_resource_loading\">Daten konnten nicht geladen werden.</string>\n    <string name=\"error_image_loading\">Bild konnte nicht geladen werden.</string>\n    <string name=\"error_mapping_loading\">Externe Seiten konnten nicht geladen werden.</string>\n    <string name=\"error_nothing_found\">Nichts gefunden.</string>\n    <string name=\"error_invalid_user\">Ungültiger Nutzer. Bist du angemeldet?</string>\n    <string name=\"error_user_update_failed\">Nutzerdaten konnten nicht aktualisiert werden.</string>\n    <string name=\"error_user_delete_waifu_failed\">Waifu/Husbando konnte nicht gelöscht werden.</string>\n    <string name=\"error_user_update_image_failed\">Profilbild oder Banner konnte nicht aktualisiert werden.</string>\n    <string name=\"error_user_create_profile_link_failed\">Profil-Link für %s konnte nicht erstellt werden.</string>\n    <string name=\"error_user_update_profile_link_failed\">Profil-Link für %s konnte nicht aktualisiert werden.</string>\n    <string name=\"error_something_wrong\">Hoppla! Etwas ist schiefgelaufen!</string>\n    <string name=\"error_search_provider_unavailable\">Suchanbieter nicht verfügbar.</string>\n    <string name=\"error_library_update_failed\">Bibliothekseintrag konnte nicht aktualisiert werden.</string>\n    <string name=\"error_library_update_failed_multiple\">%d Bibliothekseinträge konnten nicht aktualisiert werden.</string>\n    <string name=\"error_library_update_not_found\">Der Bibliothekseintrag existiert nicht.</string>\n    <string name=\"error_library_add_failed\">Hinzufügen zur Bibliothek ist fehlgeschlagen.</string>\n    <string name=\"error_library_delete_failed\">Bibliothekseintrag konnte nicht gelöscht werden.</string>\n    <string name=\"error_requires_notification_permission\">Berechtigung für Benachrichtigungen ist erforderlich.</string>\n    <string name=\"sort_average_rating_asc\">Am schlechtesten bewertet</string>\n    <string name=\"sort_average_rating_desc\">Am besten bewertet</string>\n    <string name=\"sort_release_date_asc\">Am äldesten</string>\n    <string name=\"title_media_type\">Medienart</string>\n    <string name=\"title_character_appearances\">Charakterauftritte</string>\n    <string name=\"sort_popularity_asc\">Am unbeliebtesten</string>\n    <string name=\"title_details\">Details</string>\n    <string name=\"title_streamer\">Streaming Links</string>\n    <string name=\"title_ratings\">Bewertungen</string>\n    <string name=\"title_more_franchise\">Mehr von dieser Serie</string>\n    <string name=\"title_favorite_anime\">Lieblings Anime</string>\n    <string name=\"title_favorite_manga\">Lieblings Manga</string>\n    <string name=\"title_favorite_characters\">Lieblings Charaktere</string>\n    <string name=\"title_authentication\">Authentikation</string>\n    <string name=\"title_login\">In deinem Kitsu Account anmelden</string>\n    <string name=\"title_about_me\">Über Mich</string>\n    <string name=\"title_episodes\">Episoden</string>\n    <string name=\"title_chapters\">Kapitel</string>\n    <string name=\"title_characters\">Charaktere</string>\n    <string name=\"title_edit_library_entry\">Bibliothekseintrag bearbeiten</string>\n    <string name=\"no_information\">Unbekannt</string>\n    <string name=\"no_data_available\">Keine Daten vorhanden</string>\n    <string name=\"status_current\">Wird derzeit ausgestrahlt</string>\n    <string name=\"status_current_manga\">Wird derzeit veröffentlicht</string>\n    <string name=\"status_finished\">Abgeschlossen</string>\n    <string name=\"status_tba\">TBA</string>\n    <string name=\"status_unreleased\">Unveröffentlicht</string>\n    <string name=\"character_role_main\">Haupt</string>\n    <string name=\"character_role_supporting\">Unterstützend</string>\n    <string name=\"character_role_recurring\">Wiederkehrend</string>\n    <string name=\"character_role_cameo\">Cameo</string>\n    <string name=\"library_status_watching\">Angefangen</string>\n    <string name=\"library_status_planned\">Gemerkt</string>\n    <string name=\"library_status_completed\">Abgeschlossen</string>\n    <string name=\"library_status_on_hold\">Pausiert</string>\n    <string name=\"library_status_reading\">Angefangen</string>\n    <string name=\"status_upcoming\">Bevorstehend</string>\n    <string name=\"data_popularity\">Bekanntheit #%d</string>\n    <string name=\"data_kitsu_score\">Kitsu Score %s%%</string>\n    <string name=\"data_length_total\">%s insgesamt</string>\n    <string name=\"data_length_each\">jeweils %d Minuten</string>\n    <string name=\"data_title_english\">Englisch</string>\n    <string name=\"data_title_japanese\">Japanisch</string>\n    <string name=\"data_title_japanese_romaji\">Japanisch (Romaji)</string>\n    <string name=\"data_abbreviated_titles\">Synonyme</string>\n    <string name=\"data_other_names\">Auch bekannt als</string>\n    <string name=\"data_title_type\">Typ</string>\n    <string name=\"data_title_status\">Status</string>\n    <string name=\"data_title_aired\">Ausgestrahlt</string>\n    <string name=\"data_title_tba\">Erwartet</string>\n    <string name=\"data_title_season\">Season</string>\n    <string name=\"data_title_age_rating\">Einstufung</string>\n    <string name=\"data_title_serialization\">Serialisierung</string>\n    <string name=\"data_title_chapters\">Kapitel</string>\n    <string name=\"season_winter\">Winter</string>\n    <string name=\"season_spring\">Frühling</string>\n    <string name=\"season_summer\">Sommer</string>\n    <string name=\"season_fall\">Herbst</string>\n    <string name=\"relationship_sequel\">Sequel</string>\n    <string name=\"relationship_prequel\">Prequel</string>\n    <string name=\"relationship_alternative_setting\">Alt. Setting</string>\n    <string name=\"relationship_alternative_version\">Alt. Version</string>\n    <string name=\"relationship_parent_story\">Haupthandlung</string>\n    <string name=\"relationship_side_story\">Nebenhandlung</string>\n    <string name=\"relationship_summary\">Zusammenfassung</string>\n    <string name=\"relationship_full_story\">Volle Geschichte</string>\n    <string name=\"relationship_spinoff\">Spin-off</string>\n    <string name=\"relationship_adaptation\">Adaption</string>\n    <string name=\"relationship_character\">Charakter</string>\n    <string name=\"relationship_other\">Anderes</string>\n    <string name=\"section_top_upcoming_anime\">Top bevorstehende Anime</string>\n    <string name=\"section_most_popular_anime\">Beliebteste Anime</string>\n    <string name=\"section_top_airing_manga\">Top veröffentlichte Manga</string>\n    <string name=\"section_highest_rated_anime\">Bestbewertete Anime</string>\n    <string name=\"section_most_popular_manga\">Beliebteste Manga</string>\n    <string name=\"preference_category_account\">Benutzerkonto</string>\n    <string name=\"preference_category_advanced\">Erweitert</string>\n    <string name=\"preference_dark_mode\">Dunkler Modus</string>\n    <string name=\"preference_dark_mode_dark\">Dunkel</string>\n    <string name=\"preference_dark_mode_follow_system\">System folgen</string>\n    <string name=\"preference_dark_mode_battery_saver\">Batteriesparmodus</string>\n    <string name=\"preference_dynamic_color_theme\">Benutze Dynamische Farben</string>\n    <string name=\"preference_app_theme_default\">Standard</string>\n    <string name=\"preference_app_theme_purple\">Lila</string>\n    <string name=\"preference_app_theme_blue\">Blau</string>\n    <string name=\"preference_media_item_size\">Poster-Bildgröße</string>\n    <string name=\"preference_media_item_size_small\">Klein</string>\n    <string name=\"preference_media_item_size_medium\">Mittel</string>\n    <string name=\"preference_media_item_size_large\">Groß</string>\n    <string name=\"preference_start_fragment_home\">Start</string>\n    <string name=\"preference_language\">Sprache</string>\n    <string name=\"preference_display_name\">Anzeigename</string>\n    <string name=\"preference_profile_url\">Profil-URL</string>\n    <string name=\"preference_country\">Land</string>\n    <string name=\"preference_country_summary\">%s ist als Land festgelegt</string>\n    <string name=\"preference_profile_url_not_set\">Keine Profil-URL festgelegt.</string>\n    <string name=\"preference_start_fragment_description\">%s ist als Standardseite festgelegt.</string>\n    <string name=\"preference_titles\">Titel</string>\n    <string name=\"preference_titles_canonical\">Kanonisch</string>\n    <string name=\"preference_adult_content\">Inhalte für Erwachsene</string>\n    <string name=\"preference_adult_content_description_everywhere\">Medien mit R18+ Altersbeschränkung sind überall sichtbar.</string>\n    <string name=\"preference_adult_content_description_sfw\">Medien mit R18+ Altersbeschränkung werden versteckt.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Alle Inhalte für Erwachsene verstecken</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Auf gefolgten Feed beschränken</string>\n    <string name=\"preference_sfw_filter_show_all\">Inhalt für Erwachsene überall</string>\n    <string name=\"preference_rating_system\">Bewertungssystem</string>\n    <string name=\"preference_rating_system_simple\">Einfach</string>\n    <string name=\"preference_rating_system_regular\">Regulär</string>\n    <string name=\"preference_rating_system_advanced\">Fortgeschritten</string>\n    <string name=\"preference_remember_search_filters\">Suchfilter merken</string>\n    <string name=\"preference_remember_search_filters_description\">Zuletzt genutzte Suchfilter nach einem App Neustart anwenden.</string>\n    <string name=\"preference_force_legacy_image_picker\">Alte Fotoauswahl erzwingen</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Verwende die Systemdateiauswahl anstelle der neuen Fotoauswahl.</string>\n    <string name=\"preference_check_for_updates\">Beim Start nach Aktualisierungen suchen</string>\n    <string name=\"preference_check_for_updates_description\">Suche beim Start nach einer neuen Version der App.</string>\n    <string name=\"preference_app_logs_description\">Anwendungsprotokolle anzeigen und teilen.</string>\n    <string name=\"preference_app_version\">Versions Information</string>\n    <string name=\"preference_github_repo\">GitHub Repository</string>\n    <string name=\"preference_open_source_libraries_description\">Auflistung aller benutzten Open Source Bibliotheken.</string>\n    <string name=\"preference_not_logged_in\">Du musst angemeldet sein, um diese Einstellung zu bearbeiten.</string>\n    <string name=\"prompt_password\">Passwort</string>\n    <string name=\"action_sign_in\">Anmelden</string>\n    <string name=\"action_log_in\">Anmelden</string>\n    <string name=\"login_failed\">Anmeldung ist fehlgeschlagen</string>\n    <string name=\"status_logging_in\">Wird angemeldet…</string>\n    <string name=\"logged_in_success\">Als %s angemeldet.</string>\n    <string name=\"login_no_account\">Du hast kein Benutzerkonto? Erstelle eins auf <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"library_not_started\">Nicht begonnen</string>\n    <string name=\"library_not_rated\">Nicht bewertet</string>\n    <string name=\"library_not_logged_in_title\">Anmelden, um auf deine Bibliothek zuzugreifen</string>\n    <string name=\"library_not_logged_in_text\">Melde dich mit deinem Kitsu Benutzerkonto an, um deine Bibliothek zu verwalten und Zugriff auf alle Funktionen zu erhalten.</string>\n    <string name=\"library_kind_all\">Alle</string>\n    <string name=\"library_not_synchronized\">Nicht synchronisiert</string>\n    <string name=\"library_edit_status\">Bibliotheksstatus</string>\n    <string name=\"library_edit_progress\">Fortschritt</string>\n    <string name=\"library_edit_volumes\">Bände</string>\n    <string name=\"library_edit_rating\">Bewertung</string>\n    <string name=\"library_edit_rewatch_count\">Wiederholungen</string>\n    <string name=\"library_edit_reread_count\">Wiederholungen</string>\n    <string name=\"library_edit_start_rewatch\">Wiederholung beginnen</string>\n    <string name=\"library_edit_start_reread\">Wiederholung beginnen</string>\n    <string name=\"library_edit_privacy\">Privatsphäre</string>\n    <string name=\"library_edit_privacy_public\">Öffentlich</string>\n    <string name=\"library_edit_started\">Begonnen</string>\n    <string name=\"library_edit_notes\">Persönliche Notizen</string>\n    <string name=\"info_log_in_required\">Anmelden, um auf alle Funktionen zuzugreifen.</string>\n    <string name=\"info_update_checking_new_version\">Überprüfe auf eine neue App Version…</string>\n    <string name=\"info_update_new_version_available\">Neue Version verfügbar.</string>\n    <string name=\"info_update_new_version_available_text\">Version %s von Kitsune ist verfügbar.</string>\n    <string name=\"info_update_no_new_version_available\">Keine neue Version verfügbar.</string>\n    <string name=\"profile_not_logged_in_text\">Melde dich mit deinem Kitsu Benutzerkonto an oder erstelle ein neues Benutzerkonto, um auf alle Funktionen zuzugreifen.</string>\n    <string name=\"profile_anime_stats\">Anime Statistiken</string>\n    <string name=\"profile_manga_stats\">Manga Statistiken</string>\n    <string name=\"profile_stats_anime_watch_time\">%s verbracht Anime zu schauen.</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d Kapitel gelesen.</string>\n    <string name=\"profile_stats_time_spent_total\">%s insgesamt.</string>\n    <string name=\"profile_stats_completed\">%1$d abgeschlossen. Mehr als &lt;b&gt;%2$d%%&lt;/b&gt; der Nutzer.</string>\n    <string name=\"profile_data_gender\">Geschlecht</string>\n    <string name=\"profile_data_location\">Standort</string>\n    <string name=\"profile_data_birthday\">Geburtstag</string>\n    <string name=\"profile_data_join_date\">Beitrittsdatum</string>\n    <string name=\"profile_data_join_date_ago\">vor %s</string>\n    <string name=\"profile_data_private\">Ist geheim.</string>\n    <string name=\"profile_gender_male\">Männlich</string>\n    <string name=\"profile_gender_female\">Weiblich</string>\n    <string name=\"profile_gender_custom\">Benutzerdefiniert</string>\n    <string name=\"profile_gender_custom_hint\">Lege dein Geschlecht fest</string>\n    <string name=\"profile_data_waifu\">Waifu</string>\n    <string name=\"profile_data_husbando\">Husbando</string>\n    <string name=\"profile_waifu_or_husbando\">Waifu oder Husbando</string>\n    <string name=\"profile_find_waifu\">Finde deinen Traumpartner</string>\n    <string name=\"profile_data_bio\">Bio</string>\n    <string name=\"profile_cover_image_description\">Banner</string>\n    <string name=\"profile_avatar_image_description\">Profilbild</string>\n    <string name=\"profile_link_url\">URL oder Benutzername</string>\n    <string name=\"notification_updates\">Anwendungsaktualisierungen</string>\n    <string name=\"onboarding_setup_action\">Einrichtung abschließen</string>\n    <string name=\"unit_episode\">Episode %d</string>\n    <string name=\"unit_chapter\">Kapitel %d</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s Seite</item>\n        <item quantity=\"other\">%s Seiten</item>\n    </plurals>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s Sekunde</item>\n        <item quantity=\"other\">%s Sekunden</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s Minute</item>\n        <item quantity=\"other\">%s Minuten</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s Stunde</item>\n        <item quantity=\"other\">%s Stunden</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s Tag</item>\n        <item quantity=\"other\">%s Tage</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s Monat</item>\n        <item quantity=\"other\">%s Monate</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s Jahr</item>\n        <item quantity=\"other\">%s Jahre</item>\n    </plurals>\n    <string name=\"nav_home\">Start</string>\n    <string name=\"nav_search\">Suche</string>\n    <string name=\"nav_library\">Bibliothek</string>\n    <string name=\"nav_profile\">Profil</string>\n    <string name=\"nav_settings\">Einstellungen</string>\n    <string name=\"preference_language_default\">Standadsprache</string>\n    <string name=\"widget_description\">Bibliotheksfortschritte</string>\n    <string name=\"widget_empty_text\">Keine aktiven Einträge in der Bibliothek vorhanden</string>\n    <string name=\"widget_empty_action\">Bibliothek öffnen</string>\n    <string name=\"onboarding_welcome_title\">Willkommen zu Kitsune!</string>\n    <string name=\"onboarding_welcome_subtitle\">Dein persönlicher Begleiter für Anime und Manga.</string>\n    <string name=\"onboarding_welcome_feature_search\">Suchen</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Finde deine Lieblings-Anime und -Manga.</string>\n    <string name=\"onboarding_welcome_feature_explore\">Entdecken</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Entdecke neue und angesagte Titel.</string>\n    <string name=\"onboarding_welcome_feature_track\">Verfolgen</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Behalte im Auge, was du geschaut und gelesen hast.</string>\n    <string name=\"onboarding_welcome_action\">Los geht\\'s</string>\n    <string name=\"action_skip\">Überspringen</string>\n    <string name=\"action_next\">Weiter</string>\n    <string name=\"onboarding_login_title\">Bei Kitsu anmelden</string>\n    <string name=\"onboarding_login_subtitle\">Melde dich mit deinem Kitsu-Account an oder erstelle einen neuen Account, um auf alle Funktionen zugreifen zu können.</string>\n    <string name=\"onboarding_login_is_logged_in\">Du bist angemeldet</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Account erstellen</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Du wirst zu %1$s weitergeleitet, wo du einen neuen Account erstellen kannst.</string>\n    <string name=\"onboarding_setup_title\">Ersteinrichtung</string>\n    <string name=\"onboarding_setup_subtitle\">Lege deine bevorzugten Einstellungen fest, um loszulegen. Du kannst sie später jederzeit in den Einstellungen ändern.</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">Berechtigung für Benachrichtigungen ist erforderlich.</string>\n    <string name=\"onboarding_setup_updates\">Auf Aktualisierungen prüfen</string>\n    <string name=\"onboarding_setup_updates_description\">Erhalte eine Benachrichtigung, wenn eine neue Version auf GitHub verfügbar ist.</string>\n    <string name=\"onboarding_setup_title_language\">Titel Sprache</string>\n    <string name=\"onboarding_setup_title_language_description\">Wähle deine bevorzugte Sprache für Titel aus.</string>\n    <string name=\"onboarding_setup_title_language_selected\">Ausgewählt: %1$s</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"nav_home\">Inicio</string>\n    <string name=\"nav_search\">Buscar</string>\n    <string name=\"nav_library\">Biblioteca</string>\n    <string name=\"nav_profile\">Perfil</string>\n    <string name=\"nav_app_logs\">Registros de aplicaciones</string>\n    <string name=\"action_cancel\">Cancelar</string>\n    <string name=\"action_reset_filter\">Reiniciar filtro</string>\n    <string name=\"action_show_more\">Mostrar más</string>\n    <string name=\"action_show_less\">Mostrar menos</string>\n    <string name=\"action_view\">Ver</string>\n    <string name=\"action_close\">Cerrar</string>\n    <string name=\"action_log_out\">Cerrar sesión</string>\n    <string name=\"action_open_external\">Abrir enlace externo</string>\n    <string name=\"action_open_in_browser\">Abrir en el navegador</string>\n    <string name=\"action_open_on_mal\">Abrir en MyAnimeList.net</string>\n    <string name=\"action_save_changes\">Guardar cambios</string>\n    <string name=\"action_edit_profile\">Editar perfil</string>\n    <string name=\"action_update_profile\">Actualizar perfil</string>\n    <string name=\"action_add\">Añadir</string>\n    <string name=\"action_remove\">Eliminar</string>\n    <string name=\"action_update\">Actualizar</string>\n    <string name=\"action_start\">Iniciar</string>\n    <string name=\"action_allow\">Permitir</string>\n    <string name=\"action_rate\">Valorar</string>\n    <string name=\"action_update_rating\">Actualizar la puntuación</string>\n    <string name=\"action_remove_rating\">Eliminar calificación</string>\n    <string name=\"action_delete_profile_link\">Eliminar enlace de perfil</string>\n    <string name=\"hint_search\">Buscar</string>\n    <string name=\"hint_search_characters\">Buscar personajes</string>\n    <string name=\"hint_mark_watched\">Marcar como visto.</string>\n    <string name=\"hint_rating\">Calificación</string>\n    <string name=\"dialog_log_out_confirmation\">¿Estas seguro en cerrar sesión?</string>\n    <string name=\"dialog_remove_from_library_title\">¿Remover de la biblioteca?</string>\n    <string name=\"dialog_remove_from_library_msg\"><b>%s</b> será removido de la biblioteca.</string>\n    <string name=\"nav_settings\">Ajustes</string>\n    <string name=\"nav_os_libraries\">Bibliotecas de código abierto</string>\n    <string name=\"nav_appearance\">Apariencia</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"action_read_less\">Leer menos</string>\n    <string name=\"manga\">Manga</string>\n    <string name=\"action_retry\">Reintentar</string>\n    <string name=\"action_ok\">OK</string>\n    <string name=\"action_unselect_all\">Deseleccionar todo</string>\n    <string name=\"action_share_profile_url\">Compartir perfil</string>\n    <string name=\"action_read_more\">Leer más</string>\n    <string name=\"action_back\">Atrás</string>\n    <string name=\"action_share\">Compartir</string>\n    <string name=\"action_open_external_short\">Enlaces externos</string>\n    <string name=\"action_share_app_logs\">Compartir registros</string>\n    <string name=\"action_add_to_favorites\">Agregar a favoritos</string>\n    <string name=\"action_remove_from_favorites\">Quitar de favoritos</string>\n    <string name=\"action_dismiss\">Descartar</string>\n    <string name=\"action_synchronize\">Sincronizar</string>\n    <string name=\"action_save_in_gallery\">Guardar en la galería</string>\n    <string name=\"action_add_profile_link\">Añadir enlace del perfil</string>\n    <string name=\"action_select_profile_link_site\">Seleccionar enlace</string>\n    <string name=\"action_edit_profile_link\">Editar enlace de perfil</string>\n    <string name=\"hint_mark_not_watched\">Marcar como no visto.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Permiso rechazado</string>\n    <string name=\"dialog_notification_permission_rejected\">No recibirás notificaciones ni se te informará sobre las actualizaciones de la aplicación.</string>\n    <string name=\"dialog_request_notification_permission_title\">Permitir notificaciones</string>\n    <string name=\"dialog_request_notification_permission\">Permitir notificaciones push para recibir notificaciones cuando se lanza una nueva versión de la aplicación.</string>\n    <string name=\"error_resource_loading\">No se pudieron cargar los datos.</string>\n    <string name=\"error_image_loading\">No se pudo cargar la imagen.</string>\n    <string name=\"error_mapping_loading\">No se pudieron cargar las asignaciones para sitios externos.</string>\n    <string name=\"error_nothing_found\">Sin resultados.</string>\n    <string name=\"error_invalid_user\">Usuario no válido. ¿Has iniciado sesión?</string>\n    <string name=\"error_user_update_failed\">No se pudieron actualizar los datos del usuario.</string>\n    <string name=\"error_user_delete_waifu_failed\">Error al borrar Waifu/Husbando.</string>\n    <string name=\"error_user_update_image_failed\">Error al actualizar imagen o portada de perfil.</string>\n    <string name=\"error_user_create_profile_link_failed\">Error al crear enlace de perfil para %s.</string>\n    <string name=\"error_user_update_profile_link_failed\">Error al actualizar enlace de perfil %s.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Error al borrar del perfil el enlace %s.</string>\n    <string name=\"error_something_wrong\">¡Uy, algo salió mal!</string>\n    <string name=\"error_requires_external_storage_permission\">Se necesita permiso para escribir en almacenamiento externo.</string>\n    <string name=\"error_search_provider_unavailable\">Proveedor de búsqueda no disponible.</string>\n    <string name=\"error_library_update_failed_multiple\">Error al actualizar la entrada %d en biblioteca.</string>\n    <string name=\"error_library_update_failed\">Error al actualizar entrada de biblioteca.</string>\n    <string name=\"error_library_update_not_found\">La entrada de la biblioteca no existe.</string>\n    <string name=\"error_library_add_failed\">Error al añadir a biblioteca.</string>\n    <string name=\"error_library_delete_failed\">Error al eliminar la entrada de la biblioteca.</string>\n    <string name=\"error_requires_notification_permission\">Se necesita permiso de notificación.</string>\n    <string name=\"sort_popularity_asc\">Menos popular</string>\n    <string name=\"sort_popularity_desc\">Más popular</string>\n    <string name=\"sort_average_rating_asc\">Peor valoración</string>\n    <string name=\"sort_average_rating_desc\">Mejor valoración</string>\n    <string name=\"sort_release_date_asc\">Más antiguo</string>\n    <string name=\"sort_release_date_desc\">Último</string>\n    <string name=\"title_character_appearances\">Apariciones de los personajes</string>\n    <string name=\"title_categories\">Categorías</string>\n    <string name=\"title_media_type\">Tipo de contenido</string>\n    <string name=\"title_filter\">Filtrar</string>\n    <string name=\"title_description\">Descripción</string>\n    <string name=\"title_details\">Detalles</string>\n    <string name=\"title_streamer\">Enlaces a streaming</string>\n    <string name=\"title_ratings\">Calificaciones</string>\n    <string name=\"title_more_franchise\">Más de esta serie</string>\n    <string name=\"title_favorite_anime\">Animes favoritos</string>\n    <string name=\"title_favorite_manga\">Mangas favoritos</string>\n    <string name=\"title_favorite_characters\">Personajes favoritos</string>\n    <string name=\"title_authentication\">Autenticación</string>\n    <string name=\"title_login\">Ingresa a tu cuenta de Kitsu</string>\n    <string name=\"title_play_trailer\">Reproducir tráiler</string>\n    <string name=\"title_about_me\">Sobre mí</string>\n    <string name=\"title_episodes\">Episodios</string>\n    <string name=\"title_edit_library_entry\">Editar entrada de la biblioteca</string>\n    <string name=\"title_chapters\">Capítulos</string>\n    <string name=\"title_characters\">Personajes</string>\n    <string name=\"title_edit_profile\">Editar perfil</string>\n    <string name=\"no_information\">Desconocido</string>\n    <string name=\"no_data_available\">Sin datos disponibles</string>\n    <string name=\"status_current\">En Emisión</string>\n    <string name=\"status_finished\">Terminado</string>\n    <string name=\"status_current_manga\">Publicando</string>\n    <string name=\"status_tba\">TBA</string>\n    <string name=\"status_unreleased\">Sin publicar</string>\n    <string name=\"status_upcoming\">Próximamente</string>\n    <string name=\"character_role_supporting\">Soporte</string>\n    <string name=\"character_role_main\">Principal</string>\n    <string name=\"preference_start_fragment_search\">Buscar</string>\n    <string name=\"preference_start_fragment_home\">Inicio</string>\n    <string name=\"character_role_recurring\">Recurrente</string>\n    <string name=\"character_role_cameo\">Cameo</string>\n    <string name=\"library_status_watching\">Viendo</string>\n    <string name=\"library_status_completed\">Completado</string>\n    <string name=\"library_status_on_hold\">En Pausa</string>\n    <string name=\"library_status_dropped\">Abandonado</string>\n    <string name=\"library_status_reading\">Leyendo</string>\n    <string name=\"library_status_planned_manga\">Pendiente de leer</string>\n    <string name=\"library_status_planned\">Pendiente de ver</string>\n    <string name=\"library_action_remove\">Remover de la biblioteca</string>\n    <string name=\"library_action_add\">Añadir a la biblioteca</string>\n    <string name=\"library_action_edit\">Editar entrada de la biblioteca</string>\n    <string name=\"data_rank\">Puntaje #%d</string>\n    <string name=\"data_popularity\">Popularidad #%d</string>\n    <string name=\"data_kitsu_score\">Puntuación en Kitsu %s%%</string>\n    <string name=\"data_length_total\">%s total</string>\n    <string name=\"data_length_each\">%d minutos por episodio</string>\n    <string name=\"data_title_english\">Inglés</string>\n    <string name=\"data_title_japanese\">Japonés</string>\n    <string name=\"data_title_japanese_romaji\">Japonés (Romaji)</string>\n    <string name=\"data_abbreviated_titles\">Sinónimos</string>\n    <string name=\"data_other_names\">Otros nombres</string>\n    <string name=\"data_title_type\">Tipo</string>\n    <string name=\"data_title_status\">Estado</string>\n    <string name=\"data_title_aired\">Emitido</string>\n    <string name=\"data_title_published\">Publicado</string>\n    <string name=\"data_title_tba\">Esperado</string>\n    <string name=\"data_title_season\">Temporada</string>\n    <string name=\"data_title_age_rating\">Clasificación</string>\n    <string name=\"data_title_serialization\">Serialización</string>\n    <string name=\"data_title_chapters\">Capítulos</string>\n    <string name=\"data_title_volumes\">Volúmenes</string>\n    <string name=\"data_title_episodes\">Episodios</string>\n    <string name=\"data_title_length\">Duración</string>\n    <string name=\"data_title_producers\">Productores</string>\n    <string name=\"data_title_licensors\">Licenciadores</string>\n    <string name=\"data_title_studios\">Estudios</string>\n    <string name=\"season_winter\">Invierno</string>\n    <string name=\"season_spring\">Primavera</string>\n    <string name=\"season_summer\">Verano</string>\n    <string name=\"season_fall\">Otoño</string>\n    <string name=\"relationship_sequel\">Secuela</string>\n    <string name=\"relationship_prequel\">Precuela</string>\n    <string name=\"relationship_alternative_setting\">Ajustes alternos</string>\n    <string name=\"relationship_alternative_version\">Versión alterna</string>\n    <string name=\"relationship_side_story\">Historia secundaria</string>\n    <string name=\"relationship_parent_story\">Argumento principal</string>\n    <string name=\"relationship_adaptation\">Adaptación</string>\n    <string name=\"relationship_summary\">Resumen</string>\n    <string name=\"relationship_full_story\">Historia completa</string>\n    <string name=\"relationship_spinoff\">Serie derivada de</string>\n    <string name=\"relationship_character\">Personaje</string>\n    <string name=\"relationship_other\">Otro</string>\n    <string name=\"section_trending\">Populares esta semana</string>\n    <string name=\"section_top_upcoming_anime\">Animes más esperados</string>\n    <string name=\"section_highest_rated_anime\">Animes mejor evaluados</string>\n    <string name=\"section_most_popular_anime\">Animes más populares</string>\n    <string name=\"section_top_airing_anime\">Animes más populares en emisión</string>\n    <string name=\"section_top_airing_manga\">Mangas más populares en publicación</string>\n    <string name=\"section_top_upcoming_manga\">Mangas más esperados</string>\n    <string name=\"section_highest_rated_manga\">Mangas mejor evaluados</string>\n    <string name=\"section_most_popular_manga\">Mangas más populares</string>\n    <string name=\"preference_category_ui\">Interfaz de usuario</string>\n    <string name=\"preference_category_account\">Cuenta</string>\n    <string name=\"preference_category_advanced\">Avanzado</string>\n    <string name=\"preference_category_about\">Acerca de</string>\n    <string name=\"preference_appearance_description\">Cambiar el tema y el activar modo oscuro.</string>\n    <string name=\"preference_dark_mode\">Modo Oscuro</string>\n    <string name=\"preference_dark_mode_light\">Claro</string>\n    <string name=\"preference_dark_mode_dark\">Oscuro</string>\n    <string name=\"preference_dark_mode_follow_system\">Seguir modo del sistema</string>\n    <string name=\"preference_dark_mode_battery_saver\">Ahorro de batería</string>\n    <string name=\"preference_oled_black_mode\">Tema negro OLED</string>\n    <string name=\"preference_oled_black_mode_description\">Usar fondo negro en modo oscuro.</string>\n    <string name=\"preference_app_theme\">Tema</string>\n    <string name=\"preference_dynamic_color_theme\">Usar Color Dinámico</string>\n    <string name=\"preference_dynamic_color_theme_description\">Se adapta al tema del sistema.</string>\n    <string name=\"preference_app_theme_default\">Por defecto</string>\n    <string name=\"preference_app_theme_purple\">Morado</string>\n    <string name=\"preference_app_theme_blue\">Azul</string>\n    <string name=\"preference_app_theme_green\">Verde</string>\n    <string name=\"preference_media_item_size\">Tamaño de Imagen de Póster</string>\n    <string name=\"preference_media_item_size_small\">Pequeño</string>\n    <string name=\"preference_media_item_size_medium\">Medio</string>\n    <string name=\"preference_media_item_size_large\">Grande</string>\n    <string name=\"preference_start_fragment\">Página Predeterminada</string>\n    <string name=\"preference_start_fragment_library\">Biblioteca</string>\n    <string name=\"preference_start_fragment_profile\">Perfil</string>\n    <string name=\"preference_start_fragment_description\">La página predeterminada es %s.</string>\n    <string name=\"preference_language\">Idioma</string>\n    <string name=\"preference_language_default\">Idioma predeterminado</string>\n    <string name=\"preference_display_name\">Nombre a mostrar</string>\n    <string name=\"preference_profile_url\">URL del perfil</string>\n    <string name=\"preference_profile_url_not_set\">No se ha colocado URL para el perfil.</string>\n    <string name=\"preference_country\">País</string>\n    <string name=\"preference_country_summary\">El país seleccionado es %s</string>\n    <string name=\"preference_country_summary_non\">No se ha seleccionado un país.</string>\n    <string name=\"preference_titles\">Títulos</string>\n    <string name=\"preference_titles_description\">Como se muestran los títulos de los medios.</string>\n    <string name=\"preference_titles_canonical\">Canónico</string>\n    <string name=\"preference_titles_romanized\">Romanizado</string>\n    <string name=\"preference_titles_english\">Inglés</string>\n    <string name=\"preference_adult_content\">Contenido para adultos</string>\n    <string name=\"preference_adult_content_description_sometimes\">Medios con calificación +18 serán visibles, pero se ocultarán los que posean etiqueta NSFW en feed global.</string>\n    <string name=\"preference_adult_content_description_everywhere\">Se mostrarán en todos lados medios con calificación +18.</string>\n    <string name=\"preference_adult_content_description_sfw\">Los medios con clasificación de edad +18 estarán ocultos.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Ocultar todo el contenido para adultos</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Limitar a feed de seguidos</string>\n    <string name=\"preference_sfw_filter_show_all\">Contenido adulto visible</string>\n    <string name=\"preference_rating_system\">Sistema de Calificación</string>\n    <string name=\"preference_rating_system_simple\">Sencillo</string>\n    <string name=\"preference_rating_system_regular\">Regular</string>\n    <string name=\"preference_rating_system_advanced\">Avanzado</string>\n    <string name=\"preference_remember_search_filters\">Recordar filtros de búsqueda</string>\n    <string name=\"preference_remember_search_filters_description\">Aplicar los últimos filtros de búsqueda usados después de reiniciar la aplicación.</string>\n    <string name=\"preference_force_legacy_image_picker\">Forzar Selector de Fotos heredado</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Usa el selector de fotos del sistema en lugar del nuevo.</string>\n    <string name=\"preference_check_for_updates\">Buscar Actualizaciones al Iniciar</string>\n    <string name=\"preference_check_for_updates_description\">Buscar actualizaciones de la aplicación en cada inicio.</string>\n    <string name=\"preference_app_logs_description\">Ver y compartir archivos log de la aplicación.</string>\n    <string name=\"preference_app_version\">Información de la versión</string>\n    <string name=\"preference_app_version_description\">Clic para comprobar actualizaciones.</string>\n    <string name=\"preference_github_repo\">Repositorio en GitHub</string>\n    <string name=\"preference_open_source_libraries_description\">Lista de todas las librerías open source usadas.</string>\n    <string name=\"preference_not_logged_in\">Debes iniciar sesión para cambiar esta configuración.</string>\n    <string name=\"app_logs_no_data\">No hay registros disponibles.</string>\n    <string name=\"prompt_email\">Correo electrónico</string>\n    <string name=\"prompt_password\">Contraseña</string>\n    <string name=\"action_sign_in\">Registrarse</string>\n    <string name=\"action_log_in\">Iniciar sesión</string>\n    <string name=\"action_log_in_to_kitsu\">Inicia sesión en Kitsu</string>\n    <string name=\"invalid_username\">No es un correo válido</string>\n    <string name=\"login_failed\">Error al iniciar sesión</string>\n    <string name=\"status_logging_in\">Iniciando sesión…</string>\n    <string name=\"logged_in_success\">Iniciado como %s.</string>\n    <string name=\"login_no_account\">¿No tienes cuenta? Crea una en <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"not_logged_in\">No se ha iniciado sesión</string>\n    <string name=\"library_not_started\">Sin iniciar</string>\n    <string name=\"library_not_rated\">Sin valorar</string>\n    <string name=\"library_not_logged_in_title\">Inicia sesión para acceder a tu biblioteca</string>\n    <string name=\"library_not_logged_in_text\">Ingresa en tu cuenta Kitsu para manejar tu biblioteca y tener acceso a todas las características.</string>\n    <string name=\"library_kind_all\">Todo</string>\n    <string name=\"library_not_synchronized\">Sin sincronizar</string>\n    <string name=\"library_edit_status\">Estado de la biblioteca</string>\n    <string name=\"library_edit_progress\">Progreso</string>\n    <string name=\"library_edit_volumes\">Volúmenes</string>\n    <string name=\"library_edit_rating\">Calificación</string>\n    <string name=\"library_edit_rewatch_count\">Conteo de veces visto</string>\n    <string name=\"library_edit_reread_count\">Conteo de veces leído</string>\n    <string name=\"library_edit_start_rewatch\">Iniciar de nuevo</string>\n    <string name=\"library_edit_start_reread\">Empezar a releer</string>\n    <string name=\"library_edit_privacy\">Privacidad</string>\n    <string name=\"library_edit_privacy_public\">Público</string>\n    <string name=\"library_edit_privacy_private\">Privado</string>\n    <string name=\"library_edit_started\">Iniciado</string>\n    <string name=\"library_edit_finished\">Terminado</string>\n    <string name=\"library_edit_no_date_set\">Sin fecha</string>\n    <string name=\"library_edit_notes\">Notas personales</string>\n    <string name=\"info_log_in_required\">Ingresa para acceder a todas las funciones.</string>\n    <string name=\"info_image_saved_in_gallery\">Imagen guardada en galería.</string>\n    <string name=\"info_update_checking_new_version\">Buscando nueva versión…</string>\n    <string name=\"info_update_new_version_available\">Nueva versión disponible.</string>\n    <string name=\"info_update_new_version_available_text\">Disponible la versión %s de Kitsu.</string>\n    <string name=\"info_update_no_new_version_available\">No hay nueva versión disponible.</string>\n    <string name=\"info_update_failed\">Error al buscar nueva versión.</string>\n    <string name=\"info_logged_out_token_expired\">Token de acceso expirado. Por favor ingresa de nuevo.</string>\n    <string name=\"filter_kind\">Formato</string>\n    <string name=\"filter_select_categories\">Selecciona categorías</string>\n    <string name=\"filter_year\">Año</string>\n    <string name=\"filter_avg_rating\">Promedio de Calificación</string>\n    <string name=\"filter_season\">Temporadas</string>\n    <string name=\"filter_subtype\">Subtipo</string>\n    <string name=\"filter_streamers\">Servicios de Streaming</string>\n    <string name=\"filter_age_rating\">Clasificación por edad</string>\n    <string name=\"profile_not_logged_in_title\">¡Nada por aquí!</string>\n    <string name=\"profile_not_logged_in_text\">Inicia sesión con tu cuenta de Kitsu o crea una nueva cuenta para obtener acceso a todas las funciones.</string>\n    <string name=\"profile_anime_stats\">Estadísticas del anime</string>\n    <string name=\"profile_manga_stats\">Estadísticas de Manga</string>\n    <string name=\"profile_stats_time_spent_total\">%s en total.</string>\n    <string name=\"profile_stats_anime_watch_time\">%s de anime visto.</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d capítulos leídos.</string>\n    <string name=\"profile_stats_completed\">%1$d completados. Más que el <b>%2$d%%</b> de los usuarios.</string>\n    <string name=\"profile_data_gender\">Género</string>\n    <string name=\"profile_data_location\">Ubicación</string>\n    <string name=\"profile_data_birthday\">Cumpleaños</string>\n    <string name=\"profile_data_join_date\">Fecha de registro</string>\n    <string name=\"profile_data_join_date_ago\">Hace %s</string>\n    <string name=\"profile_data_private\">Es secreto.</string>\n    <string name=\"profile_gender_male\">Hombre</string>\n    <string name=\"profile_gender_female\">Mujer</string>\n    <string name=\"profile_gender_custom\">Personalizado</string>\n    <string name=\"profile_gender_custom_hint\">Coloca tu género</string>\n    <string name=\"profile_data_waifu\">Waifu</string>\n    <string name=\"profile_data_husbando\">Husbando</string>\n    <string name=\"profile_waifu_or_husbando\">Waifu o Husbando</string>\n    <string name=\"profile_find_waifu\">Encuentra a tu persona especial</string>\n    <string name=\"profile_data_bio\">Biografía</string>\n    <string name=\"profile_cover_image_description\">Imagen de portada</string>\n    <string name=\"profile_avatar_image_description\">Imagen de perfil</string>\n    <string name=\"profile_link_url\">URL o Nombre de Usuario</string>\n    <string name=\"notification_updates\">Actualizaciones de la aplicación</string>\n    <string name=\"widget_description\">Progreso de la biblioteca</string>\n    <string name=\"unit_episode\">Episodio %d</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s Página</item>\n        <item quantity=\"many\">%s Páginas</item>\n        <item quantity=\"other\">%s Páginas</item>\n    </plurals>\n    <string name=\"unit_chapter\">Capítulo %d</string>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s segundo</item>\n        <item quantity=\"many\">%s segundos</item>\n        <item quantity=\"other\">%s segundos</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s minuto</item>\n        <item quantity=\"many\">%s minutos</item>\n        <item quantity=\"other\">%s minutos</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s hora</item>\n        <item quantity=\"many\">%s horas</item>\n        <item quantity=\"other\">%s horas</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s día</item>\n        <item quantity=\"many\">%s días</item>\n        <item quantity=\"other\">%s días</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s mes</item>\n        <item quantity=\"many\">%s meses</item>\n        <item quantity=\"other\">%s meses</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s año</item>\n        <item quantity=\"many\">%s años</item>\n        <item quantity=\"other\">%s años</item>\n    </plurals>\n    <string name=\"widget_empty_text\">No hay elementos en seguimiento en la biblioteca</string>\n    <string name=\"widget_empty_action\">Abrir biblioteca</string>\n    <string name=\"action_skip\">Omitir</string>\n    <string name=\"action_next\">Siguiente</string>\n    <string name=\"action_continue\">Continuar</string>\n    <string name=\"onboarding_welcome_title\">¡Bienvenido/-a a Kitsune!</string>\n    <string name=\"onboarding_welcome_subtitle\">Tu compañero personal de anime y manga.</string>\n    <string name=\"onboarding_welcome_feature_search\">Buscar</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Encuentra tu anime y manga favorito.</string>\n    <string name=\"onboarding_welcome_feature_explore\">Explorar</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Descubre títulos nuevos y de moda.</string>\n    <string name=\"onboarding_welcome_feature_track\">Pista</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Mantén un registro de lo que has visto y leído.</string>\n    <string name=\"onboarding_welcome_action\">Empezar</string>\n    <string name=\"onboarding_login_title\">Iniciar sesión en Kitsu</string>\n    <string name=\"onboarding_login_subtitle\">Inicia sesión en tu cuenta de Kitsu o crea una nueva cuenta para tener acceso a todas las funciones.</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Crea una cuenta</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Serás redirigido a %1$s donde podrás crear una nueva cuenta.</string>\n    <string name=\"onboarding_setup_title\">Configuración inicial</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">Se requiere permiso de notificación.</string>\n    <string name=\"onboarding_setup_updates\">Buscar actualizaciones</string>\n    <string name=\"onboarding_setup_updates_description\">Recibe notificaciones cuando una nueva versión esté disponible en GitHub.</string>\n    <string name=\"onboarding_setup_title_language\">Idioma del título</string>\n    <string name=\"onboarding_setup_title_language_description\">Selecciona el idioma preferido para los títulos.</string>\n    <string name=\"onboarding_setup_action\">Finalizar configuración</string>\n    <string name=\"action_create_account\">Crear una cuenta</string>\n    <string name=\"onboarding_login_is_logged_in\">Has iniciado sesión en</string>\n    <string name=\"onboarding_setup_subtitle\">Establece tus preferencias para comenzar. Puede cambiarlas en cualquier momento más tarde en las configuraciones.</string>\n    <string name=\"onboarding_setup_title_language_selected\">Seleccionado: %1$s</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"anime\">Animé</string>\n    <string name=\"manga\">Manga</string>\n    <string name=\"action_ok\">Ok</string>\n    <string name=\"action_cancel\">Annuler</string>\n    <string name=\"action_back\">Retour</string>\n    <string name=\"action_show_more\">Voir plus</string>\n    <string name=\"action_read_more\">Lire plus</string>\n    <string name=\"action_read_less\">Lire moins</string>\n    <string name=\"action_show_less\">Voir moins</string>\n    <string name=\"action_share_app_logs\">Partager les logs</string>\n    <string name=\"action_view\">Voir</string>\n    <string name=\"action_share_profile_url\">Partager le profil</string>\n    <string name=\"action_close\">Fermer</string>\n    <string name=\"action_log_out\">Se déconnecter</string>\n    <string name=\"action_share\">Partager</string>\n    <string name=\"action_open_external\">Ouvrir un site externe</string>\n    <string name=\"action_open_external_short\">Sites externes</string>\n    <string name=\"action_remove_from_favorites\">Retirer des favoris</string>\n    <string name=\"action_add_to_favorites\">Ajouter aux favoris</string>\n    <string name=\"nav_settings\">Paramètres</string>\n    <string name=\"nav_appearance\">Apparence</string>\n    <string name=\"nav_os_libraries\">Open Source Libraries</string>\n    <string name=\"action_retry\">Réessayer</string>\n    <string name=\"action_unselect_all\">Tout désélectionner</string>\n    <string name=\"action_dismiss\">Ignorer</string>\n    <string name=\"action_synchronize\">Synchroniser</string>\n    <string name=\"action_save_in_gallery\">Enregistrer dans la Gallerie</string>\n    <string name=\"action_open_on_mal\">Ouvrir sur MyAnimeList</string>\n    <string name=\"action_remove\">Retirer</string>\n    <string name=\"action_update\">Mettre à jour</string>\n    <string name=\"action_remove_rating\">Retirer l\\'évaluation</string>\n    <string name=\"action_edit_profile_link\">Éditer le lien du profil</string>\n    <string name=\"error_image_loading\">Échec du chargement de l\\'image.</string>\n    <string name=\"error_mapping_loading\">Échec du chargement des mappages pour les sites externes.</string>\n    <string name=\"error_search_provider_unavailable\">Service de recherche indisponible.</string>\n    <string name=\"error_library_update_failed\">Échec de la mise à jour de l\\'entréee de la bibliothèque.</string>\n    <string name=\"error_library_update_not_found\">L\\'entrée de la bibliothèque n\\'existe pas.</string>\n    <string name=\"sort_release_date_desc\">Plus récent</string>\n    <string name=\"title_media_type\">Type de média</string>\n    <string name=\"character_role_recurring\">Récurrent</string>\n    <string name=\"character_role_cameo\">Caméo</string>\n    <string name=\"library_status_completed\">Complétés</string>\n    <string name=\"data_length_total\">%s total</string>\n    <string name=\"data_length_each\">%d minutes par épisode</string>\n    <string name=\"data_title_english\">Anglais</string>\n    <string name=\"data_title_japanese\">Japonais</string>\n    <string name=\"data_title_japanese_romaji\">Japonais (Romaji)</string>\n    <string name=\"data_title_serialization\">Sérialisation</string>\n    <string name=\"season_spring\">Printemps</string>\n    <string name=\"season_summer\">Été</string>\n    <string name=\"season_fall\">Automne</string>\n    <string name=\"relationship_parent_story\">Histoire parenté</string>\n    <string name=\"relationship_summary\">Résumé</string>\n    <string name=\"relationship_full_story\">Histoire complète</string>\n    <string name=\"relationship_spinoff\">Spin-off</string>\n    <string name=\"relationship_adaptation\">Adaptation</string>\n    <string name=\"section_trending\">En tendance cette semaine</string>\n    <string name=\"section_top_airing_manga\">Top des Mangas en cours</string>\n    <string name=\"section_top_upcoming_manga\">Top des Mangas à venir</string>\n    <string name=\"preference_category_ui\">Interface utilisateur</string>\n    <string name=\"preference_dark_mode\">Mode sombre</string>\n    <string name=\"preference_dark_mode_light\">Clair</string>\n    <string name=\"preference_dark_mode_dark\">Sombre</string>\n    <string name=\"preference_dark_mode_follow_system\">Suivre le système</string>\n    <string name=\"preference_dark_mode_battery_saver\">Économie de batterie</string>\n    <string name=\"preference_dynamic_color_theme_description\">S\\'adapte au thème du système.</string>\n    <string name=\"preference_app_theme_default\">Par défaut</string>\n    <string name=\"preference_start_fragment\">Page par défaut</string>\n    <string name=\"preference_profile_url_not_set\">Pas d\\'URL de profil défini.</string>\n    <string name=\"preference_country\">Pays</string>\n    <string name=\"preference_country_summary_non\">Pas de pays spécifié.</string>\n    <string name=\"preference_titles_english\">Anglais</string>\n    <string name=\"preference_rating_system_simple\">Simple</string>\n    <string name=\"preference_force_legacy_image_picker\">Forcer le sélecteur de photos hérité</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Utilise le sélecteur de fichiers système au lieu du nouveau sélecteur de photos.</string>\n    <string name=\"preference_github_repo\">Dépôt GitHub</string>\n    <string name=\"nav_home\">Accueil</string>\n    <string name=\"nav_search\">Chercher</string>\n    <string name=\"nav_library\">Bibliothèque</string>\n    <string name=\"nav_profile\">Profil</string>\n    <string name=\"nav_app_logs\">Logs de l\\'application</string>\n    <string name=\"data_abbreviated_titles\">Synonymes</string>\n    <string name=\"data_other_names\">Autres noms</string>\n    <string name=\"data_title_type\">Type</string>\n    <string name=\"data_title_status\">Statut</string>\n    <string name=\"data_title_aired\">Diffusé</string>\n    <string name=\"relationship_sequel\">Suite</string>\n    <string name=\"relationship_alternative_setting\">Paramètre alternatif</string>\n    <string name=\"data_title_published\">Publié</string>\n    <string name=\"relationship_prequel\">Préquelle</string>\n    <string name=\"relationship_alternative_version\">Version alternative</string>\n    <string name=\"data_title_tba\">Attendu</string>\n    <string name=\"data_title_studios\">Studios</string>\n    <string name=\"relationship_side_story\">Histoire parallèle</string>\n    <string name=\"data_title_season\">Saison</string>\n    <string name=\"data_title_age_rating\">Évaluation</string>\n    <string name=\"data_title_chapters\">Chapitres</string>\n    <string name=\"data_title_volumes\">Volumes</string>\n    <string name=\"data_title_episodes\">Épisodes</string>\n    <string name=\"data_title_producers\">Producteurs</string>\n    <string name=\"relationship_character\">Personnage</string>\n    <string name=\"preference_app_theme_blue\">Bleu</string>\n    <string name=\"preference_app_theme_green\">Vert</string>\n    <string name=\"preference_media_item_size_small\">Petites</string>\n    <string name=\"data_title_length\">Longueur</string>\n    <string name=\"relationship_other\">Autre</string>\n    <string name=\"data_title_licensors\">Concédants</string>\n    <string name=\"section_top_airing_anime\">Top des animés en cours</string>\n    <string name=\"preference_media_item_size\">Taille des affiches</string>\n    <string name=\"season_winter\">Hiver</string>\n    <string name=\"section_top_upcoming_anime\">Top des animés à venir</string>\n    <string name=\"preference_media_item_size_medium\">Moyennes</string>\n    <string name=\"preference_media_item_size_large\">Grandes</string>\n    <string name=\"section_highest_rated_anime\">Animés les mieux notés</string>\n    <string name=\"section_most_popular_anime\">Animés les plus populaires</string>\n    <string name=\"preference_start_fragment_home\">Accueil</string>\n    <string name=\"preference_start_fragment_search\">Chercher</string>\n    <string name=\"section_highest_rated_manga\">Mangas les mieux notés</string>\n    <string name=\"preference_start_fragment_library\">Bibliothèque</string>\n    <string name=\"section_most_popular_manga\">Mangas les plus populaires</string>\n    <string name=\"preference_start_fragment_profile\">Profil</string>\n    <string name=\"preference_language\">Langue</string>\n    <string name=\"preference_language_default\">Langue par défaut</string>\n    <string name=\"preference_category_account\">Compte</string>\n    <string name=\"preference_oled_black_mode_description\">Utilise un fond noir en mode sombre.</string>\n    <string name=\"preference_start_fragment_description\">La page par défaut est définie sur %s.</string>\n    <string name=\"preference_category_advanced\">Avancé</string>\n    <string name=\"preference_profile_url\">URL du profil</string>\n    <string name=\"preference_titles\">Titres</string>\n    <string name=\"preference_category_about\">À propos</string>\n    <string name=\"preference_country_summary\">Le pays est défini sur %s</string>\n    <string name=\"preference_appearance_description\">Change le thème et active le mode sombre.</string>\n    <string name=\"preference_titles_description\">Définissez la manière dont les titres multimédias seront affichés.</string>\n    <string name=\"preference_oled_black_mode\">Noir OLED</string>\n    <string name=\"preference_display_name\">Pseudo</string>\n    <string name=\"preference_app_theme\">Thème</string>\n    <string name=\"preference_dynamic_color_theme\">Utilise Dynamic Color</string>\n    <string name=\"preference_app_theme_purple\">Violet</string>\n    <string name=\"action_reset_filter\">Enlever le filtre</string>\n    <string name=\"action_allow\">Autoriser</string>\n    <string name=\"action_open_in_browser\">Ouvrir dans le navigateur</string>\n    <string name=\"action_add\">Ajouter</string>\n    <string name=\"action_add_profile_link\">Ajouter un lien de profil</string>\n    <string name=\"action_save_changes\">Sauvegarder les changements</string>\n    <string name=\"action_start\">Commencer</string>\n    <string name=\"action_delete_profile_link\">Supprimer le lien du profil</string>\n    <string name=\"action_update_profile\">Mettre à jour le profil</string>\n    <string name=\"action_edit_profile\">Éditer le profil</string>\n    <string name=\"action_rate\">Évaluer</string>\n    <string name=\"action_update_rating\">Mettre à jour l\\'évaluation</string>\n    <string name=\"action_select_profile_link_site\">Séléctionner un site</string>\n    <string name=\"hint_search\">Chercher</string>\n    <string name=\"hint_search_characters\">Chercher des personnages</string>\n    <string name=\"dialog_remove_from_library_msg\">&lt;b&gt;%s&lt;/b&gt; sera retiré de la bibliothèque.</string>\n    <string name=\"hint_mark_not_watched\">Maquer comme non vu.</string>\n    <string name=\"dialog_remove_from_library_title\">Retirer de la bibliothèque ?</string>\n    <string name=\"hint_mark_watched\">Marquer comme vu.</string>\n    <string name=\"hint_rating\">Évaluation</string>\n    <string name=\"dialog_log_out_confirmation\">Êtes-vous sûr de vouloir vous déconnecter ?</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Permission refusée</string>\n    <string name=\"dialog_notification_permission_rejected\">Vous n\\'aurez pas de notifications et ne serez pas informé des mise à jours de l\\'application.</string>\n    <string name=\"dialog_request_notification_permission_title\">Autoriser les notifications</string>\n    <string name=\"dialog_request_notification_permission\">Autoriser les notifications push pour être notifier quand une mise à jour de l\\'application est disponible.</string>\n    <string name=\"error_resource_loading\">Impossible de charger les données.</string>\n    <string name=\"error_nothing_found\">Aucun résultat.</string>\n    <string name=\"error_invalid_user\">Utilisateur invalide ? Êtes-vous connecté ?</string>\n    <string name=\"error_something_wrong\">Oups, quelque chose a cloché !</string>\n    <string name=\"error_user_update_failed\">Échec de la mise à jour des données utilisateur.</string>\n    <string name=\"error_user_update_image_failed\">Échec de la mise à jour de l\\'image de profil ou de la couverture.</string>\n    <string name=\"error_requires_external_storage_permission\">Autorisation requise pour écrire sur un stockage externe.</string>\n    <string name=\"error_user_delete_waifu_failed\">Échec de la suppression de la Waifu / du Husbando.</string>\n    <string name=\"error_user_create_profile_link_failed\">Échec de la création du lien de profil pour %s.</string>\n    <string name=\"error_user_update_profile_link_failed\">Échec de la mise à jour du lien de profil pour %s.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Échec de la suppression du lien de profil pour %s.</string>\n    <string name=\"error_library_update_failed_multiple\">Échec de la mise à jour des entrées %d de la bibliothèque.</string>\n    <string name=\"error_library_delete_failed\">Échec de la suppression de l\\'entrée de la bibliothèque.</string>\n    <string name=\"error_library_add_failed\">Échec de l\\'ajout à votre bibliothèque.</string>\n    <string name=\"error_requires_notification_permission\">Autorisation de notification requise.</string>\n    <string name=\"sort_popularity_desc\">Plus populaires</string>\n    <string name=\"sort_popularity_asc\">Moins populaires</string>\n    <string name=\"sort_average_rating_asc\">Pires évaluations</string>\n    <string name=\"sort_average_rating_desc\">Meilleures évaluations</string>\n    <string name=\"sort_release_date_asc\">Plus ancien</string>\n    <string name=\"title_character_appearances\">Apparences du personnage</string>\n    <string name=\"title_filter\">Filtre</string>\n    <string name=\"title_categories\">Catégories</string>\n    <string name=\"title_description\">Description</string>\n    <string name=\"title_details\">Détails</string>\n    <string name=\"title_streamer\">Liens de streaming</string>\n    <string name=\"title_ratings\">Évaluations</string>\n    <string name=\"title_favorite_characters\">Personnages favoris</string>\n    <string name=\"title_more_franchise\">Plus de cette série</string>\n    <string name=\"title_favorite_anime\">Animés favoris</string>\n    <string name=\"title_favorite_manga\">Mangas favoris</string>\n    <string name=\"title_authentication\">Authentification</string>\n    <string name=\"title_login\">Connexion à votre compte Kitsu</string>\n    <string name=\"title_play_trailer\">Lire la bande annonce</string>\n    <string name=\"title_chapters\">Chapitres</string>\n    <string name=\"title_about_me\">À propos de moi</string>\n    <string name=\"title_episodes\">Épisodes</string>\n    <string name=\"title_characters\">Personnages</string>\n    <string name=\"no_information\">Inconnu</string>\n    <string name=\"title_edit_library_entry\">Éditer l\\'entré de la bibliothèque</string>\n    <string name=\"status_current\">En cours de diffusion</string>\n    <string name=\"status_tba\">Annoncé</string>\n    <string name=\"status_unreleased\">Non commencé</string>\n    <string name=\"character_role_main\">Principal</string>\n    <string name=\"character_role_supporting\">Secondaire</string>\n    <string name=\"title_edit_profile\">Éditer le profil</string>\n    <string name=\"no_data_available\">Pas de données disponibles</string>\n    <string name=\"status_current_manga\">En cours d\\'édition</string>\n    <string name=\"status_finished\">Terminé</string>\n    <string name=\"status_upcoming\">À venir</string>\n    <string name=\"library_status_planned\">À regarder</string>\n    <string name=\"library_status_watching\">En cours de visionnage</string>\n    <string name=\"library_status_on_hold\">En pause</string>\n    <string name=\"library_status_dropped\">Abandonnés</string>\n    <string name=\"library_status_planned_manga\">Veux lire</string>\n    <string name=\"library_status_reading\">En cours de lecture</string>\n    <string name=\"library_action_add\">Ajouter à la bibliothèque</string>\n    <string name=\"library_action_edit\">Éditer l\\'entrée de la bibliothèque</string>\n    <string name=\"data_rank\">Classement #%d</string>\n    <string name=\"library_action_remove\">Retirer de la bibliothèque</string>\n    <string name=\"data_popularity\">Popularité #%d</string>\n    <string name=\"data_kitsu_score\">Score Kitsu %s%%</string>\n    <string name=\"preference_titles_romanized\">Romanisé</string>\n    <string name=\"preference_titles_canonical\">Canonique</string>\n    <string name=\"preference_adult_content\">Contenu adulte</string>\n    <string name=\"preference_remember_search_filters\">Se souvenir des filtres de recherche</string>\n    <string name=\"preference_remember_search_filters_description\">Appliquer les derniers filtres de recherche après le rédémarage de l\\'application.</string>\n    <string name=\"preference_adult_content_description_sfw\">Les médias classés par âge R18+ seront masqués.</string>\n    <string name=\"preference_adult_content_description_sometimes\">Les médias classés par âge R18+ seront visibles, mais les publications marquées NSFW seront masquées dans le flux global.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Masquer tout le contenu adulte</string>\n    <string name=\"preference_adult_content_description_everywhere\">Les médias classés par âge R18+ seront visibles partout.</string>\n    <string name=\"preference_rating_system_regular\">Normal</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Limiter au flux suivi</string>\n    <string name=\"preference_sfw_filter_show_all\">Contenu pour adultes partout</string>\n    <string name=\"preference_rating_system\">Système d\\'évaluation</string>\n    <string name=\"preference_rating_system_advanced\">Avancé</string>\n    <string name=\"preference_check_for_updates\">Vérifier les mise à jour au démarrage</string>\n    <string name=\"preference_app_version\">Infos sur la version</string>\n    <string name=\"preference_check_for_updates_description\">Vérifie si une nouvelle version de l\\'appication est disponible à chaque lancement.</string>\n    <string name=\"preference_app_logs_description\">Voir et partager les messages de log.</string>\n    <string name=\"preference_app_version_description\">Cliquez pour vérifier les mises à jours.</string>\n    <string name=\"preference_open_source_libraries_description\">Liste de toutes les bibliothèques open source utilisées.</string>\n    <string name=\"preference_not_logged_in\">Vous devez être connecté pour éditer ce paramètre.</string>\n    <string name=\"action_log_in_to_kitsu\">Connexion à Kitsu</string>\n    <string name=\"invalid_username\">L\\'adresse email n\\'est pas valide</string>\n    <string name=\"not_logged_in\">Non connecté</string>\n    <string name=\"library_not_started\">Non commencé</string>\n    <string name=\"library_not_rated\">Non évalué</string>\n    <string name=\"library_kind_all\">Tout</string>\n    <string name=\"library_not_synchronized\">Non synchronisé</string>\n    <string name=\"library_edit_status\">Statut dans la bibliothèque</string>\n    <string name=\"library_edit_progress\">Progression</string>\n    <string name=\"profile_gender_male\">Homme</string>\n    <string name=\"profile_gender_female\">Femme</string>\n    <string name=\"profile_gender_custom\">Personnalisé</string>\n    <string name=\"profile_gender_custom_hint\">Définissez votre genre</string>\n    <string name=\"profile_data_waifu\">Waifu</string>\n    <string name=\"profile_data_husbando\">Husbando</string>\n    <string name=\"profile_cover_image_description\">Bannière</string>\n    <string name=\"notification_updates\">Mises à jour de l\\'application</string>\n    <string name=\"unit_episode\">Épisode %d</string>\n    <string name=\"unit_chapter\">Chapitre %d</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s Page</item>\n        <item quantity=\"many\">%s Pages</item>\n        <item quantity=\"other\">%s Pages</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s mois</item>\n        <item quantity=\"many\">%s mois</item>\n        <item quantity=\"other\">%s mois</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s an</item>\n        <item quantity=\"many\">%s ans</item>\n        <item quantity=\"other\">%s ans</item>\n    </plurals>\n    <string name=\"library_edit_rewatch_count\">Nombre de revisionnage(s)</string>\n    <string name=\"library_edit_started\">Commencé</string>\n    <string name=\"prompt_password\">Mot de passe</string>\n    <string name=\"login_failed\">La connexion a échoué</string>\n    <string name=\"logged_in_success\">Connecté en tant que %s.</string>\n    <string name=\"app_logs_no_data\">Pas de logs disponible.</string>\n    <string name=\"prompt_email\">Email</string>\n    <string name=\"status_logging_in\">Connexion…</string>\n    <string name=\"action_sign_in\">S\\'inscrire</string>\n    <string name=\"action_log_in\">Se connecter</string>\n    <string name=\"login_no_account\">Vous n\\'avez pas de compte ? Créez en un <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"library_not_logged_in_title\">Connectez vous pour accéder à votre bibliothèque</string>\n    <string name=\"library_edit_volumes\">Volumes</string>\n    <string name=\"library_not_logged_in_text\">Connectez-vous à votre compte Kitsu pour gérer votre bibliothèque et accéder à toutes les fonctionnalités.</string>\n    <string name=\"library_edit_rating\">Évaluation</string>\n    <string name=\"library_edit_reread_count\">Nombre de relecture(s)</string>\n    <string name=\"library_edit_start_rewatch\">Commencer le revisionnage</string>\n    <string name=\"library_edit_start_reread\">Commencer la relecture</string>\n    <string name=\"library_edit_privacy\">Confidentialité</string>\n    <string name=\"library_edit_privacy_public\">Publique</string>\n    <string name=\"library_edit_privacy_private\">Privé</string>\n    <string name=\"library_edit_finished\">Terminé</string>\n    <string name=\"library_edit_no_date_set\">Pas de date définie</string>\n    <string name=\"library_edit_notes\">Notes personnelles</string>\n    <string name=\"info_log_in_required\">Connectez-vous pour accéder à toutes les fonctionnalités.</string>\n    <string name=\"info_image_saved_in_gallery\">Image enregistrée avec succès dans la galerie.</string>\n    <string name=\"info_update_checking_new_version\">Recherche d\\'une nouvelle version…</string>\n    <string name=\"info_update_new_version_available\">Une nouvelle version est disponible.</string>\n    <string name=\"info_update_new_version_available_text\">La version %s de Kitsune est disponible.</string>\n    <string name=\"info_update_no_new_version_available\">Pas de nouvelle version disponible.</string>\n    <string name=\"info_update_failed\">Échec de la recherche de nouvelle version.</string>\n    <string name=\"info_logged_out_token_expired\">Votre access token est expiré. Merci de vous reconnecter.</string>\n    <string name=\"filter_kind\">Genre</string>\n    <string name=\"filter_year\">Année</string>\n    <string name=\"filter_avg_rating\">Évaluation moyenne</string>\n    <string name=\"filter_select_categories\">Selectionnez des catégories</string>\n    <string name=\"filter_season\">Saison</string>\n    <string name=\"filter_subtype\">Sous type</string>\n    <string name=\"filter_streamers\">Plateformes de streaming</string>\n    <string name=\"profile_anime_stats\">Statistiques des animés</string>\n    <string name=\"profile_manga_stats\">Statistiques des mangas</string>\n    <string name=\"profile_stats_anime_watch_time\">%s passées à regarder des animés.</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d chapitres lus.</string>\n    <string name=\"profile_stats_time_spent_total\">%s au total.</string>\n    <string name=\"profile_stats_completed\">%1$d complétés. C\\'est plus que &lt;b&gt;%2$d%%&lt;/b&gt; des utilisateurs.</string>\n    <string name=\"profile_data_gender\">Genre</string>\n    <string name=\"profile_data_location\">Localisation</string>\n    <string name=\"profile_data_birthday\">Anniversaire</string>\n    <string name=\"profile_data_join_date\">Date d\\'inscription</string>\n    <string name=\"profile_data_join_date_ago\">Il y a %s</string>\n    <string name=\"filter_age_rating\">Tranches d\\'âge</string>\n    <string name=\"profile_not_logged_in_title\">Rien à voir ici !</string>\n    <string name=\"profile_not_logged_in_text\">Connectez-vous à votre compte Kitsu ou créez un nouveau compte pour accéder à toutes les fonctionnalités.</string>\n    <string name=\"profile_data_private\">C\\'est un secret.</string>\n    <string name=\"profile_waifu_or_husbando\">Waifu ou Husbando</string>\n    <string name=\"profile_data_bio\">Biographie</string>\n    <string name=\"profile_find_waifu\">Trouvez votre quelqu\\'un spécial</string>\n    <string name=\"profile_avatar_image_description\">Image de profil</string>\n    <string name=\"profile_link_url\">URL ou Pseudo</string>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s seconde</item>\n        <item quantity=\"many\">%s secondes</item>\n        <item quantity=\"other\">%s secondes</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s minute</item>\n        <item quantity=\"many\">%s minutes</item>\n        <item quantity=\"other\">%s minutes</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s heure</item>\n        <item quantity=\"many\">%s heures</item>\n        <item quantity=\"other\">%s heures</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s jour</item>\n        <item quantity=\"many\">%s jours</item>\n        <item quantity=\"other\">%s jours</item>\n    </plurals>\n    <string name=\"widget_description\">Progression de la bibliothèque</string>\n    <string name=\"widget_empty_text\">Pas d\\'activité suivie dans votre bibliothèque</string>\n    <string name=\"widget_empty_action\">Bibliothèque ouverte</string>\n    <string name=\"action_create_account\">Créer un compte</string>\n    <string name=\"onboarding_welcome_subtitle\">Votre compagnon personnel pour les animés et mangas.</string>\n    <string name=\"onboarding_welcome_feature_search\">Rechercher</string>\n    <string name=\"onboarding_welcome_feature_explore\">Explorer</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Découvrir des titres nouveaux et tendances.</string>\n    <string name=\"action_skip\">Passer</string>\n    <string name=\"action_next\">Suivant</string>\n    <string name=\"onboarding_welcome_title\">Bienvenue sur Kitsune !</string>\n    <string name=\"action_continue\">Continuer</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Trouver vos animés et mangas favoris.</string>\n    <string name=\"onboarding_welcome_feature_track\">Suivre</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Suivez ce que vous avez lu et regardé.</string>\n    <string name=\"onboarding_welcome_action\">Commencer</string>\n    <string name=\"onboarding_login_title\">Connectez-vous à Kitsu</string>\n    <string name=\"onboarding_login_subtitle\">Connectez-vous à votre compte Kitsu ou créez en un nouveau pour accéder à toutes les fonctionnalités.</string>\n    <string name=\"onboarding_login_is_logged_in\">Connecté</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Créer un compte</string>\n    <string name=\"onboarding_setup_title_language\">Langue des titres</string>\n    <string name=\"onboarding_setup_title_language_description\">Sélectionner la langue d\\'affichage des titres.</string>\n    <string name=\"onboarding_setup_title_language_selected\">Sélectionné : %1$s</string>\n    <string name=\"onboarding_setup_action\">Configuration terminée</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Vous allez être redirigé vers %1$s où vous pourrez créer un nouveau compte.</string>\n    <string name=\"onboarding_setup_title\">Configuration initiale</string>\n    <string name=\"onboarding_setup_subtitle\">Choisissez vos paramètres pour commencer. Vous pourrez les changer à n\\'importe quel moment dans les paramètres.</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">L\\'autorisation de Notification est requise.</string>\n    <string name=\"onboarding_setup_updates\">Vérifier les mises à jour</string>\n    <string name=\"onboarding_setup_updates_description\">Soyez notifié dès qu\\'une nouvelle version est disponible sur GitHub.</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"error_search_provider_unavailable\">Ricerca provider non disponibile.</string>\n    <string name=\"nav_home\">Home</string>\n    <string name=\"nav_search\">Cerca</string>\n    <string name=\"nav_library\">Libreria</string>\n    <string name=\"nav_profile\">Profilo</string>\n    <string name=\"nav_appearance\">Aspetto</string>\n    <string name=\"nav_app_logs\">Log dell\\'applicazione</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"manga\">Manga</string>\n    <string name=\"action_cancel\">Annulla</string>\n    <string name=\"action_reset_filter\">Azzera filtro</string>\n    <string name=\"action_unselect_all\">Deseleziona tutto</string>\n    <string name=\"action_read_more\">Leggi di più</string>\n    <string name=\"action_read_less\">Leggi di meno</string>\n    <string name=\"action_view\">Visualizza</string>\n    <string name=\"action_back\">Indietro</string>\n    <string name=\"action_skip\">Salta</string>\n    <string name=\"action_next\">Successivo</string>\n    <string name=\"action_log_out\">Esci</string>\n    <string name=\"action_share_profile_url\">Condividi profilo</string>\n    <string name=\"action_share\">Condividi</string>\n    <string name=\"action_open_external\">Apri in un sito esterno</string>\n    <string name=\"action_open_external_short\">Sito esterni</string>\n    <string name=\"action_share_app_logs\">Condividi log</string>\n    <string name=\"action_add_to_favorites\">Aggiungi ai preferiti</string>\n    <string name=\"action_remove_from_favorites\">Rimuovi dai preferiti</string>\n    <string name=\"action_dismiss\">Ignora</string>\n    <string name=\"action_synchronize\">Sincronizza</string>\n    <string name=\"action_open_on_mal\">Apri in MyAnimeList.net</string>\n    <string name=\"action_save_changes\">Salva modifiche</string>\n    <string name=\"action_edit_profile\">Modifica profilo</string>\n    <string name=\"action_update_profile\">Aggiorna profilo</string>\n    <string name=\"action_add\">Aggiungi</string>\n    <string name=\"action_remove\">Rimuovi</string>\n    <string name=\"action_update\">Aggiorna</string>\n    <string name=\"action_rate\">Valuta</string>\n    <string name=\"action_update_rating\">Aggiorna valutazione</string>\n    <string name=\"action_add_profile_link\">Aggiungi link profilo</string>\n    <string name=\"action_select_profile_link_site\">Seleziona un sito</string>\n    <string name=\"hint_search\">Cerca</string>\n    <string name=\"hint_search_characters\">Cerca personaggi</string>\n    <string name=\"hint_mark_watched\">Contrassegna come visto.</string>\n    <string name=\"hint_rating\">Valutazione</string>\n    <string name=\"dialog_remove_from_library_msg\"><b>%s</b> verrà rimosso dalla tua libreria.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Autorizzazione rifiutata</string>\n    <string name=\"dialog_request_notification_permission_title\">Consenti notifiche</string>\n    <string name=\"dialog_request_notification_permission\">Consenti le notifiche push per essere avvisato quando viene rilasciata una nuova versione dell\\'app.</string>\n    <string name=\"error_resource_loading\">Impossibile caricare i dati.</string>\n    <string name=\"error_image_loading\">Impossibile caricare l\\'immagine.</string>\n    <string name=\"error_nothing_found\">Nessun risultato trovato.</string>\n    <string name=\"error_invalid_user\">Utente non valido. Hai effettuato l\\'accesso?</string>\n    <string name=\"error_user_update_failed\">Impossibile aggiornare i dati utente.</string>\n    <string name=\"error_something_wrong\">Oops, qualcosa è andato storto!</string>\n    <string name=\"error_requires_external_storage_permission\">È necessario il permesso di scrittura sulla memoria esterna.</string>\n    <string name=\"error_user_delete_waifu_failed\">Impossibile eliminare Waifu/Husbando.</string>\n    <string name=\"error_user_update_image_failed\">Impossibile aggiornare l\\'immagine profilo o copertina.</string>\n    <string name=\"error_user_create_profile_link_failed\">Impossibile creare il link profilo per %s .</string>\n    <string name=\"error_user_update_profile_link_failed\">Impossibile aggiornare il link profilo per %s.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Impossibile eliminare il link profilo per %s.</string>\n    <string name=\"error_library_update_not_found\">L\\'elemento della libreria non esiste.</string>\n    <string name=\"error_library_update_failed_multiple\">Impossibile aggiornare %d elementi della libreria.</string>\n    <string name=\"error_library_add_failed\">Impossibile aggiungere alla tua libreria.</string>\n    <string name=\"error_library_delete_failed\">Impossibile eliminare elemento della libreria.</string>\n    <string name=\"error_requires_notification_permission\">Autorizzazione alle notifiche richiesta.</string>\n    <string name=\"sort_popularity_asc\">Meno popolare</string>\n    <string name=\"sort_popularity_desc\">Più popolare</string>\n    <string name=\"sort_average_rating_asc\">Peggior punteggio</string>\n    <string name=\"sort_average_rating_desc\">Miglior punteggio</string>\n    <string name=\"sort_release_date_desc\">Più recente</string>\n    <string name=\"title_media_type\">Tipologia media</string>\n    <string name=\"title_character_appearances\">Aspetti del personaggio</string>\n    <string name=\"title_categories\">Categorie</string>\n    <string name=\"title_description\">Descrizione</string>\n    <string name=\"title_streamer\">Link streaming</string>\n    <string name=\"title_ratings\">Valutazioni</string>\n    <string name=\"title_more_franchise\">Altro da questa serie</string>\n    <string name=\"title_favorite_anime\">Anime preferiti</string>\n    <string name=\"title_authentication\">Autenticazione</string>\n    <string name=\"title_login\">Accedi al tuo account Kitsu</string>\n    <string name=\"title_play_trailer\">Riproduci trailer</string>\n    <string name=\"title_about_me\">Su di me</string>\n    <string name=\"title_episodes\">Episodi</string>\n    <string name=\"title_characters\">Personaggi</string>\n    <string name=\"no_information\">Sconosciuto</string>\n    <string name=\"no_data_available\">Nessun dato disponibile</string>\n    <string name=\"status_current_manga\">In pubblicazione</string>\n    <string name=\"status_current\">In onda</string>\n    <string name=\"status_finished\">Terminato</string>\n    <string name=\"status_tba\">Da annunciare</string>\n    <string name=\"status_unreleased\">Non pubblicato</string>\n    <string name=\"status_upcoming\">In arrivo</string>\n    <string name=\"character_role_main\">Principale</string>\n    <string name=\"character_role_supporting\">Secondario</string>\n    <string name=\"character_role_recurring\">Ricorrente</string>\n    <string name=\"library_status_completed\">Completato</string>\n    <string name=\"library_status_planned\">Da vedere</string>\n    <string name=\"library_status_on_hold\">In pausa</string>\n    <string name=\"library_status_reading\">Leggendo</string>\n    <string name=\"library_action_add\">Aggiungi alla libreria</string>\n    <string name=\"library_action_edit\">Modifica elemento della libreria</string>\n    <string name=\"data_rank\">Posizione #%d</string>\n    <string name=\"data_kitsu_score\">Punteggio Kitsu %s%%</string>\n    <string name=\"data_length_each\">%d minuti per episodio</string>\n    <string name=\"data_abbreviated_titles\">Sinonimi</string>\n    <string name=\"data_other_names\">Conosciuto anche come</string>\n    <string name=\"data_title_aired\">Trasmesso</string>\n    <string name=\"data_title_published\">Pubblicato</string>\n    <string name=\"data_title_tba\">Previsto</string>\n    <string name=\"data_title_season\">Stagione</string>\n    <string name=\"data_title_chapters\">Capitoli</string>\n    <string name=\"data_title_episodes\">Episodi</string>\n    <string name=\"data_title_length\">Durata</string>\n    <string name=\"data_title_studios\">Studi</string>\n    <string name=\"relationship_sequel\">Sequel</string>\n    <string name=\"relationship_prequel\">Prequel</string>\n    <string name=\"relationship_alternative_version\">Versione alt.</string>\n    <string name=\"relationship_side_story\">Storia secondaria</string>\n    <string name=\"relationship_parent_story\">Storia principale</string>\n    <string name=\"relationship_full_story\">Storia completa</string>\n    <string name=\"relationship_adaptation\">Adattamento</string>\n    <string name=\"relationship_other\">Altro</string>\n    <string name=\"section_top_airing_anime\">Anime più popolari in corso</string>\n    <string name=\"section_highest_rated_anime\">Anime con valutazione più alta</string>\n    <string name=\"section_top_airing_manga\">Manga più popolari in pubblicazione</string>\n    <string name=\"section_highest_rated_manga\">Manga con valutazione più alta</string>\n    <string name=\"preference_category_ui\">Interfaccia utente</string>\n    <string name=\"preference_category_advanced\">Avanzate</string>\n    <string name=\"preference_category_about\">Informazioni su</string>\n    <string name=\"preference_appearance_description\">Cambia il tema e abilita la modalità scura.</string>\n    <string name=\"preference_dark_mode_dark\">Scuro</string>\n    <string name=\"preference_dark_mode_light\">Chiaro</string>\n    <string name=\"preference_dark_mode_follow_system\">Segui il sistema</string>\n    <string name=\"nav_settings\">Impostazioni</string>\n    <string name=\"nav_os_libraries\">Librerie open source</string>\n    <string name=\"action_retry\">Riprova</string>\n    <string name=\"action_ok\">OK</string>\n    <string name=\"action_show_more\">Mostra di più</string>\n    <string name=\"action_show_less\">Mostra di meno</string>\n    <string name=\"action_continue\">Continua</string>\n    <string name=\"action_close\">Chiudi</string>\n    <string name=\"action_open_in_browser\">Apri nel browser</string>\n    <string name=\"action_save_in_gallery\">Salva nella galleria</string>\n    <string name=\"dialog_log_out_confirmation\">Sei sicuro di voler uscire?</string>\n    <string name=\"action_start\">Inizia</string>\n    <string name=\"action_allow\">Consenti</string>\n    <string name=\"action_remove_rating\">Rimuovi valutazione</string>\n    <string name=\"action_edit_profile_link\">Modifica link profilo</string>\n    <string name=\"action_delete_profile_link\">Elimina link profilo</string>\n    <string name=\"hint_mark_not_watched\">Contrassegna come non visto.</string>\n    <string name=\"dialog_remove_from_library_title\">Rimuovere dalla libreria?</string>\n    <string name=\"dialog_notification_permission_rejected\">Non riceverai notifiche e non sarai informato sugli aggiornamenti dell\\'app.</string>\n    <string name=\"error_mapping_loading\">Impossibile caricare mapping per siti esterni.</string>\n    <string name=\"error_library_update_failed\">Impossibile aggiornare gli elementi della libreria.</string>\n    <string name=\"title_favorite_characters\">Personaggi preferiti</string>\n    <string name=\"sort_release_date_asc\">Meno recente</string>\n    <string name=\"title_details\">Dettagli</string>\n    <string name=\"title_filter\">Filtro</string>\n    <string name=\"title_chapters\">Capitoli</string>\n    <string name=\"title_favorite_manga\">Manga preferiti</string>\n    <string name=\"title_edit_library_entry\">Modifica elementi della libreria</string>\n    <string name=\"title_edit_profile\">Modifica profilo</string>\n    <string name=\"character_role_cameo\">Apparizione</string>\n    <string name=\"library_status_watching\">Visione in corso</string>\n    <string name=\"library_status_dropped\">Abbandonato</string>\n    <string name=\"library_status_planned_manga\">Da leggere</string>\n    <string name=\"library_action_remove\">Rimuovi dalla libreria</string>\n    <string name=\"data_popularity\">Popolarità #%d</string>\n    <string name=\"data_length_total\">%s totale</string>\n    <string name=\"data_title_status\">Stato</string>\n    <string name=\"data_title_english\">Inglese</string>\n    <string name=\"data_title_japanese\">Giapponese</string>\n    <string name=\"data_title_japanese_romaji\">Giapponese (Romaji)</string>\n    <string name=\"data_title_type\">Tipo</string>\n    <string name=\"data_title_age_rating\">Valutazione</string>\n    <string name=\"data_title_volumes\">Volumi</string>\n    <string name=\"data_title_producers\">Produttori</string>\n    <string name=\"data_title_serialization\">Serializzazione</string>\n    <string name=\"data_title_licensors\">Licenziatari</string>\n    <string name=\"relationship_character\">Personaggio</string>\n    <string name=\"season_winter\">Inverno</string>\n    <string name=\"season_summer\">Estate</string>\n    <string name=\"section_top_upcoming_anime\">Anime più attesi</string>\n    <string name=\"season_fall\">Autunno</string>\n    <string name=\"season_spring\">Primavera</string>\n    <string name=\"relationship_alternative_setting\">Impostazioni alt.</string>\n    <string name=\"section_most_popular_manga\">Manga più popolari</string>\n    <string name=\"relationship_spinoff\">Spin-off</string>\n    <string name=\"relationship_summary\">Riassunto</string>\n    <string name=\"section_trending\">In tendenza questa settimana</string>\n    <string name=\"section_most_popular_anime\">Anime più popolari</string>\n    <string name=\"section_top_upcoming_manga\">Manga più attesi</string>\n    <string name=\"preference_dark_mode\">Modalità scura</string>\n    <string name=\"preference_dark_mode_battery_saver\">Risparmio batteria</string>\n    <string name=\"preference_oled_black_mode\">Nero OLED</string>\n    <string name=\"preference_category_account\">Account</string>\n    <string name=\"preference_oled_black_mode_description\">Utilizza un sfondo nero in modalità scura.</string>\n    <string name=\"preference_app_theme\">Tema</string>\n    <string name=\"preference_dynamic_color_theme\">Utilizza Dynamic Color</string>\n    <string name=\"preference_dynamic_color_theme_description\">Usa il tema del dispositivo.</string>\n    <string name=\"preference_app_theme_default\">Default</string>\n    <string name=\"preference_media_item_size\">Dimensione immagine poster</string>\n    <string name=\"preference_media_item_size_small\">Piccola</string>\n    <string name=\"preference_start_fragment_home\">Home</string>\n    <string name=\"preference_start_fragment_search\">Cerca</string>\n    <string name=\"preference_start_fragment\">Pagina predefinita</string>\n    <string name=\"preference_start_fragment_description\">La pagina predefinita è impostata su %s.</string>\n    <string name=\"preference_start_fragment_library\">Libreria</string>\n    <string name=\"preference_language\">Lingua</string>\n    <string name=\"preference_country_summary_non\">Nessun paese specificato.</string>\n    <string name=\"preference_titles\">Titoli</string>\n    <string name=\"preference_titles_romanized\">Romanizzato</string>\n    <string name=\"preference_titles_english\">Inglese</string>\n    <string name=\"preference_adult_content\">Contenuto per adulti</string>\n    <string name=\"preference_adult_content_description_sfw\">I media con classificazione R18+ verrano nascosti.</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Limita al feed dei seguiti</string>\n    <string name=\"preference_rating_system\">Sistema di valutazione</string>\n    <string name=\"preference_rating_system_simple\">Semplice</string>\n    <string name=\"preference_remember_search_filters\">Ricorda i filtri di ricerca</string>\n    <string name=\"preference_remember_search_filters_description\">Applica gli ultimi filtri di ricerca utilizzati dopo il riavvio dell\\'app.</string>\n    <string name=\"preference_force_legacy_image_picker\">Forza il selettore foto classico</string>\n    <string name=\"preference_check_for_updates\">Controlla aggiornamenti all\\'avvio</string>\n    <string name=\"preference_github_repo\">Repository GitHub</string>\n    <string name=\"preference_app_version\">Informazioni sulla versione</string>\n    <string name=\"app_logs_no_data\">Nessun log disponibile.</string>\n    <string name=\"prompt_password\">Password</string>\n    <string name=\"action_sign_in\">Iscriviti</string>\n    <string name=\"action_log_in\">Accedi</string>\n    <string name=\"action_log_in_to_kitsu\">Accedi a Kitsu</string>\n    <string name=\"action_create_account\">Crea un account</string>\n    <string name=\"invalid_username\">Indirizzo email non valido</string>\n    <string name=\"login_failed\">Accesso fallito</string>\n    <string name=\"status_logging_in\">Accesso in corso…</string>\n    <string name=\"not_logged_in\">Non connesso</string>\n    <string name=\"library_not_rated\">Non valutato</string>\n    <string name=\"library_not_logged_in_title\">Connettiti per accedere alla tua libreria</string>\n    <string name=\"library_edit_reread_count\">Numero di letture</string>\n    <string name=\"library_edit_rewatch_count\">Numero di visioni</string>\n    <string name=\"library_edit_start_reread\">Iniziai a rileggere</string>\n    <string name=\"library_edit_no_date_set\">Nessuna data impostata</string>\n    <string name=\"info_update_checking_new_version\">Verifica disponibilità aggiornamento…</string>\n    <string name=\"info_update_new_version_available_text\">È disponibile la versione %s di Kitsune.</string>\n    <string name=\"info_update_no_new_version_available\">Non è disponibile una nuova versione.</string>\n    <string name=\"info_update_failed\">Impossibile verificare disponibilità aggiornamento.</string>\n    <string name=\"info_logged_out_token_expired\">Il tuo token di accesso è scaduto. Per favore, accedi di nuovo.</string>\n    <string name=\"filter_select_categories\">Seleziona categorie</string>\n    <string name=\"filter_streamers\">Servizi di streaming</string>\n    <string name=\"filter_age_rating\">Classificazione per età</string>\n    <string name=\"profile_not_logged_in_title\">Niente da vedere qui!</string>\n    <string name=\"profile_anime_stats\">Statistiche anime</string>\n    <string name=\"profile_manga_stats\">Statistiche manga</string>\n    <string name=\"profile_stats_anime_watch_time\">%s trascorsi a guardare anime.</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d capitoli letti.</string>\n    <string name=\"profile_stats_time_spent_total\">%s in totale.</string>\n    <string name=\"profile_data_location\">Posizione</string>\n    <string name=\"profile_data_birthday\">Compleanno</string>\n    <string name=\"profile_data_join_date_ago\">%s fa</string>\n    <string name=\"profile_gender_male\">Maschio</string>\n    <string name=\"profile_gender_female\">Femmina</string>\n    <string name=\"profile_waifu_or_husbando\">Waifu o Husbando</string>\n    <string name=\"profile_data_husbando\">Husbando</string>\n    <string name=\"profile_avatar_image_description\">Immagine profilo</string>\n    <string name=\"profile_link_url\">URL o nome utente</string>\n    <string name=\"widget_empty_text\">Non ci sono elementi attivi tracciati nella tua libreria</string>\n    <string name=\"widget_empty_action\">Apri Libreria</string>\n    <string name=\"onboarding_welcome_title\">Benvenuto su Kitsune!</string>\n    <string name=\"onboarding_welcome_subtitle\">Il tuo compagno personale per anime e manga.</string>\n    <string name=\"onboarding_welcome_feature_explore\">Esplora</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Scopri titoli nuovi e di tendenza.</string>\n    <string name=\"onboarding_welcome_feature_track\">Traccia</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Tieni traccia di cosa guardi e leggi.</string>\n    <string name=\"onboarding_login_title\">Accedi a Kitsu</string>\n    <string name=\"onboarding_login_subtitle\">Accedi al tuo account Kitsu o creane uno per avere accesso al tutte le funzionalità.</string>\n    <string name=\"onboarding_login_is_logged_in\">Hai effettuato l\\'accesso</string>\n    <string name=\"onboarding_setup_title\">Configurazione inziale</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Sarai reindirizzato verso %1$s dove potrai creare un nuovo account.</string>\n    <string name=\"onboarding_setup_updates\">Verifica disponibilità aggiornamenti</string>\n    <string name=\"onboarding_setup_updates_description\">Ricevi una notifica quando una nuova versione è disponibile su GitHub.</string>\n    <string name=\"onboarding_setup_title_language\">Lingua titoli</string>\n    <string name=\"onboarding_setup_title_language_selected\">Selezionata: %1$s</string>\n    <string name=\"onboarding_setup_action\">Completa la configurazione</string>\n    <string name=\"unit_episode\">Episodio %d</string>\n    <string name=\"unit_chapter\">Capitolo %d</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s Pagina</item>\n        <item quantity=\"many\">%s Pagine</item>\n        <item quantity=\"other\">%s Pagine</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s minuto</item>\n        <item quantity=\"many\">%s minuti</item>\n        <item quantity=\"other\">%s minuti</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s ora</item>\n        <item quantity=\"many\">%s ore</item>\n        <item quantity=\"other\">%s ore</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s mese</item>\n        <item quantity=\"many\">%s mesi</item>\n        <item quantity=\"other\">%s mesi</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s anno</item>\n        <item quantity=\"many\">%s anni</item>\n        <item quantity=\"other\">%s anni</item>\n    </plurals>\n    <string name=\"preference_start_fragment_profile\">Profilo</string>\n    <string name=\"preference_app_theme_purple\">Viola</string>\n    <string name=\"preference_app_theme_green\">Verde</string>\n    <string name=\"preference_app_theme_blue\">Blu</string>\n    <string name=\"preference_media_item_size_medium\">Media</string>\n    <string name=\"preference_media_item_size_large\">Grande</string>\n    <string name=\"preference_language_default\">Lingua predefinita</string>\n    <string name=\"preference_profile_url\">URL del profilo</string>\n    <string name=\"preference_display_name\">Nome visualizzato</string>\n    <string name=\"preference_profile_url_not_set\">L\\'URL del profilo non è impostato.</string>\n    <string name=\"preference_country\">Paese</string>\n    <string name=\"preference_country_summary\">Paese impostato su %s</string>\n    <string name=\"preference_adult_content_description_sometimes\">I media con classificazione R18+ saranno visibili, ma i post contrassegnati come NSFW saranno nascosti nel feed globale.</string>\n    <string name=\"preference_titles_description\">Definisci la visualizzazione dei titoli dei media.</string>\n    <string name=\"preference_titles_canonical\">Canonico</string>\n    <string name=\"preference_adult_content_description_everywhere\">I media con classificazione R18+ saranno visibili ovunque.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Nascondi tutti i contenuti per adulti</string>\n    <string name=\"preference_sfw_filter_show_all\">Contenuti per adulti ovunque</string>\n    <string name=\"preference_rating_system_regular\">Normale</string>\n    <string name=\"preference_rating_system_advanced\">Avanzato</string>\n    <string name=\"preference_check_for_updates_description\">Controlla se è disponibile una nuova versione dell\\'app ad ogni avvio.</string>\n    <string name=\"preference_app_version_description\">Clicca per cercare aggiornamenti.</string>\n    <string name=\"preference_not_logged_in\">Devi avere effettuato l\\'accesso per modificare questa impostazione.</string>\n    <string name=\"preference_app_logs_description\">Vedi e condividi i messaggi log dell\\'app.</string>\n    <string name=\"preference_open_source_libraries_description\">Una lista di tutte le librerie open source utilizzate.</string>\n    <string name=\"prompt_email\">Email</string>\n    <string name=\"logged_in_success\">Accesso effettuato come %s.</string>\n    <string name=\"login_no_account\">Non hai un account? Creane uno su <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"library_not_started\">Non iniziato</string>\n    <string name=\"library_not_synchronized\">Non sincronizzato</string>\n    <string name=\"library_not_logged_in_text\">Accedi al tuo account Kitsu per gestire la tua libreria e avere accesso a tutte le funzionalità.</string>\n    <string name=\"library_kind_all\">Tutto</string>\n    <string name=\"library_edit_status\">Stato libreria</string>\n    <string name=\"library_edit_progress\">Progresso</string>\n    <string name=\"library_edit_volumes\">Volumi</string>\n    <string name=\"library_edit_rating\">Valutazione</string>\n    <string name=\"library_edit_start_rewatch\">Inizia a rivedere</string>\n    <string name=\"library_edit_privacy\">Privacy</string>\n    <string name=\"library_edit_started\">Iniziato</string>\n    <string name=\"library_edit_privacy_public\">Pubblico</string>\n    <string name=\"library_edit_privacy_private\">Privato</string>\n    <string name=\"library_edit_finished\">Finito</string>\n    <string name=\"library_edit_notes\">Note personali</string>\n    <string name=\"filter_kind\">Tipologia</string>\n    <string name=\"info_log_in_required\">Accedi per sfruttare tutte le funzionalità.</string>\n    <string name=\"info_image_saved_in_gallery\">Immagine salvata con successo nella galleria.</string>\n    <string name=\"info_update_new_version_available\">Nuova versione disponibile.</string>\n    <string name=\"filter_year\">Anno</string>\n    <string name=\"filter_avg_rating\">Valutazione media</string>\n    <string name=\"filter_season\">Stagione</string>\n    <string name=\"filter_subtype\">Sottotipo</string>\n    <string name=\"profile_stats_completed\">%1$d completati. Più del <b>%2$d%%</b> degli utenti.</string>\n    <string name=\"profile_data_gender\">Genere</string>\n    <string name=\"profile_data_join_date\">Data di iscrizione</string>\n    <string name=\"profile_data_private\">È un segreto.</string>\n    <string name=\"profile_gender_custom\">Personalizzato</string>\n    <string name=\"profile_data_waifu\">Waifu</string>\n    <string name=\"profile_gender_custom_hint\">Definisci il tuo genere</string>\n    <string name=\"profile_find_waifu\">Trova la tua anima gemella</string>\n    <string name=\"profile_data_bio\">Bio</string>\n    <string name=\"profile_cover_image_description\">Immagine di copertina</string>\n    <string name=\"notification_updates\">Aggiornamenti dell\\'app</string>\n    <string name=\"widget_description\">Progresso della libreria</string>\n    <string name=\"onboarding_welcome_feature_search\">Cerca</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Trova i tuoi anime e manga preferiti.</string>\n    <string name=\"onboarding_welcome_action\">Comincia</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Crea un account</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">È necessaria l\\'autorizzazione per le notifiche.</string>\n    <string name=\"onboarding_setup_subtitle\">Imposta le tue preferenze per iniziare. Puoi cambiarle in ogni momento nelle impostazioni.</string>\n    <string name=\"onboarding_setup_title_language_description\">Seleziona la tua lingua preferita per i titoli.</string>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s secondo</item>\n        <item quantity=\"many\">%s secondi</item>\n        <item quantity=\"other\">%s secondi</item>\n    </plurals>\n    <string name=\"preference_force_legacy_image_picker_description\">Utilizza il selettore foto di sistema invece del nuovo selettore foto.</string>\n    <string name=\"profile_not_logged_in_text\">Accedi al tuo account Kitsu o creane uno per avere accesso a tutte le funzionalità.</string>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s giorno</item>\n        <item quantity=\"many\">%s giorni</item>\n        <item quantity=\"other\">%s giorni</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-land/dimens.xml",
    "content": "<resources>\n    <dimen name=\"activity_horizontal_margin\">48dp</dimen>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- M3 Default Dark Theme Colors -->\n    <color name=\"md_theme_primary\">#FFB4A7</color>\n    <color name=\"md_theme_onPrimary\">#561E15</color>\n    <color name=\"md_theme_primaryContainer\">#733429</color>\n    <color name=\"md_theme_onPrimaryContainer\">#FFDAD4</color>\n    <color name=\"md_theme_secondary\">#E7BDB5</color>\n    <color name=\"md_theme_onSecondary\">#442A25</color>\n    <color name=\"md_theme_secondaryContainer\">#5D3F3A</color>\n    <color name=\"md_theme_onSecondaryContainer\">#FFDAD4</color>\n    <color name=\"md_theme_tertiary\">#DDC48C</color>\n    <color name=\"md_theme_onTertiary\">#3D2E04</color>\n    <color name=\"md_theme_tertiaryContainer\">#564519</color>\n    <color name=\"md_theme_onTertiaryContainer\">#FAE0A6</color>\n    <color name=\"md_theme_error\">#FFB4AB</color>\n    <color name=\"md_theme_onError\">#690005</color>\n    <color name=\"md_theme_errorContainer\">#93000A</color>\n    <color name=\"md_theme_onErrorContainer\">#FFDAD6</color>\n    <color name=\"md_theme_background\">#1A1110</color>\n    <color name=\"md_theme_onBackground\">#F1DFDB</color>\n    <color name=\"md_theme_surface\">#1A1110</color>\n    <color name=\"md_theme_onSurface\">#F1DFDB</color>\n    <color name=\"md_theme_surfaceVariant\">#534340</color>\n    <color name=\"md_theme_onSurfaceVariant\">#D8C2BE</color>\n    <color name=\"md_theme_outline\">#A08C89</color>\n    <color name=\"md_theme_outlineVariant\">#534340</color>\n    <color name=\"md_theme_scrim\">#000000</color>\n    <color name=\"md_theme_inverseSurface\">#F1DFDB</color>\n    <color name=\"md_theme_inverseOnSurface\">#392E2C</color>\n    <color name=\"md_theme_inversePrimary\">#904B3F</color>\n    <color name=\"md_theme_primaryFixed\">#FFDAD4</color>\n    <color name=\"md_theme_onPrimaryFixed\">#3A0A04</color>\n    <color name=\"md_theme_primaryFixedDim\">#FFB4A7</color>\n    <color name=\"md_theme_onPrimaryFixedVariant\">#733429</color>\n    <color name=\"md_theme_secondaryFixed\">#FFDAD4</color>\n    <color name=\"md_theme_onSecondaryFixed\">#2C1511</color>\n    <color name=\"md_theme_secondaryFixedDim\">#E7BDB5</color>\n    <color name=\"md_theme_onSecondaryFixedVariant\">#5D3F3A</color>\n    <color name=\"md_theme_tertiaryFixed\">#FAE0A6</color>\n    <color name=\"md_theme_onTertiaryFixed\">#251A00</color>\n    <color name=\"md_theme_tertiaryFixedDim\">#DDC48C</color>\n    <color name=\"md_theme_onTertiaryFixedVariant\">#564519</color>\n    <color name=\"md_theme_surfaceDim\">#1A1110</color>\n    <color name=\"md_theme_surfaceBright\">#423735</color>\n    <color name=\"md_theme_surfaceContainerLowest\">#140C0B</color>\n    <color name=\"md_theme_surfaceContainerLow\">#231918</color>\n    <color name=\"md_theme_surfaceContainer\">#271D1C</color>\n    <color name=\"md_theme_surfaceContainerHigh\">#322826</color>\n    <color name=\"md_theme_surfaceContainerHighest\">#3D3230</color>\n    <color name=\"md_theme_primary_mediumContrast\">#FFBAAE</color>\n    <color name=\"md_theme_onPrimary_mediumContrast\">#330502</color>\n    <color name=\"md_theme_primaryContainer_mediumContrast\">#CC7B6D</color>\n    <color name=\"md_theme_onPrimaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_theme_secondary_mediumContrast\">#ECC1B9</color>\n    <color name=\"md_theme_onSecondary_mediumContrast\">#26100C</color>\n    <color name=\"md_theme_secondaryContainer_mediumContrast\">#AE8881</color>\n    <color name=\"md_theme_onSecondaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_theme_tertiary_mediumContrast\">#E1C890</color>\n    <color name=\"md_theme_onTertiary_mediumContrast\">#1E1500</color>\n    <color name=\"md_theme_tertiaryContainer_mediumContrast\">#A48E5B</color>\n    <color name=\"md_theme_onTertiaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_theme_error_mediumContrast\">#FFBAB1</color>\n    <color name=\"md_theme_onError_mediumContrast\">#370001</color>\n    <color name=\"md_theme_errorContainer_mediumContrast\">#FF5449</color>\n    <color name=\"md_theme_onErrorContainer_mediumContrast\">#000000</color>\n    <color name=\"md_theme_background_mediumContrast\">#1A1110</color>\n    <color name=\"md_theme_onBackground_mediumContrast\">#F1DFDB</color>\n    <color name=\"md_theme_surface_mediumContrast\">#1A1110</color>\n    <color name=\"md_theme_onSurface_mediumContrast\">#FFF9F8</color>\n    <color name=\"md_theme_surfaceVariant_mediumContrast\">#534340</color>\n    <color name=\"md_theme_onSurfaceVariant_mediumContrast\">#DCC6C2</color>\n    <color name=\"md_theme_outline_mediumContrast\">#B39E9B</color>\n    <color name=\"md_theme_outlineVariant_mediumContrast\">#927F7B</color>\n    <color name=\"md_theme_scrim_mediumContrast\">#000000</color>\n    <color name=\"md_theme_inverseSurface_mediumContrast\">#F1DFDB</color>\n    <color name=\"md_theme_inverseOnSurface_mediumContrast\">#322826</color>\n    <color name=\"md_theme_inversePrimary_mediumContrast\">#74352A</color>\n    <color name=\"md_theme_primaryFixed_mediumContrast\">#FFDAD4</color>\n    <color name=\"md_theme_onPrimaryFixed_mediumContrast\">#2C0200</color>\n    <color name=\"md_theme_primaryFixedDim_mediumContrast\">#FFB4A7</color>\n    <color name=\"md_theme_onPrimaryFixedVariant_mediumContrast\">#5E241A</color>\n    <color name=\"md_theme_secondaryFixed_mediumContrast\">#FFDAD4</color>\n    <color name=\"md_theme_onSecondaryFixed_mediumContrast\">#200B08</color>\n    <color name=\"md_theme_secondaryFixedDim_mediumContrast\">#E7BDB5</color>\n    <color name=\"md_theme_onSecondaryFixedVariant_mediumContrast\">#4B2F2A</color>\n    <color name=\"md_theme_tertiaryFixed_mediumContrast\">#FAE0A6</color>\n    <color name=\"md_theme_onTertiaryFixed_mediumContrast\">#181000</color>\n    <color name=\"md_theme_tertiaryFixedDim_mediumContrast\">#DDC48C</color>\n    <color name=\"md_theme_onTertiaryFixedVariant_mediumContrast\">#443409</color>\n    <color name=\"md_theme_surfaceDim_mediumContrast\">#1A1110</color>\n    <color name=\"md_theme_surfaceBright_mediumContrast\">#423735</color>\n    <color name=\"md_theme_surfaceContainerLowest_mediumContrast\">#140C0B</color>\n    <color name=\"md_theme_surfaceContainerLow_mediumContrast\">#231918</color>\n    <color name=\"md_theme_surfaceContainer_mediumContrast\">#271D1C</color>\n    <color name=\"md_theme_surfaceContainerHigh_mediumContrast\">#322826</color>\n    <color name=\"md_theme_surfaceContainerHighest_mediumContrast\">#3D3230</color>\n    <color name=\"md_theme_primary_highContrast\">#FFF9F8</color>\n    <color name=\"md_theme_onPrimary_highContrast\">#000000</color>\n    <color name=\"md_theme_primaryContainer_highContrast\">#FFBAAE</color>\n    <color name=\"md_theme_onPrimaryContainer_highContrast\">#000000</color>\n    <color name=\"md_theme_secondary_highContrast\">#FFF9F8</color>\n    <color name=\"md_theme_onSecondary_highContrast\">#000000</color>\n    <color name=\"md_theme_secondaryContainer_highContrast\">#ECC1B9</color>\n    <color name=\"md_theme_onSecondaryContainer_highContrast\">#000000</color>\n    <color name=\"md_theme_tertiary_highContrast\">#FFFAF6</color>\n    <color name=\"md_theme_onTertiary_highContrast\">#000000</color>\n    <color name=\"md_theme_tertiaryContainer_highContrast\">#E1C890</color>\n    <color name=\"md_theme_onTertiaryContainer_highContrast\">#000000</color>\n    <color name=\"md_theme_error_highContrast\">#FFF9F9</color>\n    <color name=\"md_theme_onError_highContrast\">#000000</color>\n    <color name=\"md_theme_errorContainer_highContrast\">#FFBAB1</color>\n    <color name=\"md_theme_onErrorContainer_highContrast\">#000000</color>\n    <color name=\"md_theme_background_highContrast\">#1A1110</color>\n    <color name=\"md_theme_onBackground_highContrast\">#F1DFDB</color>\n    <color name=\"md_theme_surface_highContrast\">#1A1110</color>\n    <color name=\"md_theme_onSurface_highContrast\">#FFFFFF</color>\n    <color name=\"md_theme_surfaceVariant_highContrast\">#534340</color>\n    <color name=\"md_theme_onSurfaceVariant_highContrast\">#FFF9F8</color>\n    <color name=\"md_theme_outline_highContrast\">#DCC6C2</color>\n    <color name=\"md_theme_outlineVariant_highContrast\">#DCC6C2</color>\n    <color name=\"md_theme_scrim_highContrast\">#000000</color>\n    <color name=\"md_theme_inverseSurface_highContrast\">#F1DFDB</color>\n    <color name=\"md_theme_inverseOnSurface_highContrast\">#000000</color>\n    <color name=\"md_theme_inversePrimary_highContrast\">#4E180F</color>\n    <color name=\"md_theme_primaryFixed_highContrast\">#FFE0DA</color>\n    <color name=\"md_theme_onPrimaryFixed_highContrast\">#000000</color>\n    <color name=\"md_theme_primaryFixedDim_highContrast\">#FFBAAE</color>\n    <color name=\"md_theme_onPrimaryFixedVariant_highContrast\">#330502</color>\n    <color name=\"md_theme_secondaryFixed_highContrast\">#FFE0DA</color>\n    <color name=\"md_theme_onSecondaryFixed_highContrast\">#000000</color>\n    <color name=\"md_theme_secondaryFixedDim_highContrast\">#ECC1B9</color>\n    <color name=\"md_theme_onSecondaryFixedVariant_highContrast\">#26100C</color>\n    <color name=\"md_theme_tertiaryFixed_highContrast\">#FFE4AA</color>\n    <color name=\"md_theme_onTertiaryFixed_highContrast\">#000000</color>\n    <color name=\"md_theme_tertiaryFixedDim_highContrast\">#E1C890</color>\n    <color name=\"md_theme_onTertiaryFixedVariant_highContrast\">#1E1500</color>\n    <color name=\"md_theme_surfaceDim_highContrast\">#1A1110</color>\n    <color name=\"md_theme_surfaceBright_highContrast\">#423735</color>\n    <color name=\"md_theme_surfaceContainerLowest_highContrast\">#140C0B</color>\n    <color name=\"md_theme_surfaceContainerLow_highContrast\">#231918</color>\n    <color name=\"md_theme_surfaceContainer_highContrast\">#271D1C</color>\n    <color name=\"md_theme_surfaceContainerHigh_highContrast\">#322826</color>\n    <color name=\"md_theme_surfaceContainerHighest_highContrast\">#3D3230</color>\n    \n    <!-- M3 Purple Dark Theme Colors -->\n    <color name=\"md_purple_theme_primary\">#EDB4EB</color>\n    <color name=\"md_purple_theme_onPrimary\">#4A204D</color>\n    <color name=\"md_purple_theme_primaryContainer\">#633664</color>\n    <color name=\"md_purple_theme_onPrimaryContainer\">#FFD6FB</color>\n    <color name=\"md_purple_theme_secondary\">#D8BFD4</color>\n    <color name=\"md_purple_theme_onSecondary\">#3C2B3B</color>\n    <color name=\"md_purple_theme_secondaryContainer\">#534152</color>\n    <color name=\"md_purple_theme_onSecondaryContainer\">#F5DBF1</color>\n    <color name=\"md_purple_theme_tertiary\">#F6B8AC</color>\n    <color name=\"md_purple_theme_onTertiary\">#4C251E</color>\n    <color name=\"md_purple_theme_tertiaryContainer\">#673B33</color>\n    <color name=\"md_purple_theme_onTertiaryContainer\">#FFDAD3</color>\n    <color name=\"md_purple_theme_error\">#FFB4AB</color>\n    <color name=\"md_purple_theme_onError\">#690005</color>\n    <color name=\"md_purple_theme_errorContainer\">#93000A</color>\n    <color name=\"md_purple_theme_onErrorContainer\">#FFDAD6</color>\n    <color name=\"md_purple_theme_background\">#171216</color>\n    <color name=\"md_purple_theme_onBackground\">#EBDFE6</color>\n    <color name=\"md_purple_theme_surface\">#171216</color>\n    <color name=\"md_purple_theme_onSurface\">#EBDFE6</color>\n    <color name=\"md_purple_theme_surfaceVariant\">#4D444C</color>\n    <color name=\"md_purple_theme_onSurfaceVariant\">#D0C3CC</color>\n    <color name=\"md_purple_theme_outline\">#998D96</color>\n    <color name=\"md_purple_theme_outlineVariant\">#4D444C</color>\n    <color name=\"md_purple_theme_scrim\">#000000</color>\n    <color name=\"md_purple_theme_inverseSurface\">#EBDFE6</color>\n    <color name=\"md_purple_theme_inverseOnSurface\">#352E34</color>\n    <color name=\"md_purple_theme_inversePrimary\">#7D4E7E</color>\n    <color name=\"md_purple_theme_primaryFixed\">#FFD6FB</color>\n    <color name=\"md_purple_theme_onPrimaryFixed\">#320936</color>\n    <color name=\"md_purple_theme_primaryFixedDim\">#EDB4EB</color>\n    <color name=\"md_purple_theme_onPrimaryFixedVariant\">#633664</color>\n    <color name=\"md_purple_theme_secondaryFixed\">#F5DBF1</color>\n    <color name=\"md_purple_theme_onSecondaryFixed\">#261626</color>\n    <color name=\"md_purple_theme_secondaryFixedDim\">#D8BFD4</color>\n    <color name=\"md_purple_theme_onSecondaryFixedVariant\">#534152</color>\n    <color name=\"md_purple_theme_tertiaryFixed\">#FFDAD3</color>\n    <color name=\"md_purple_theme_onTertiaryFixed\">#33110B</color>\n    <color name=\"md_purple_theme_tertiaryFixedDim\">#F6B8AC</color>\n    <color name=\"md_purple_theme_onTertiaryFixedVariant\">#673B33</color>\n    <color name=\"md_purple_theme_surfaceDim\">#171216</color>\n    <color name=\"md_purple_theme_surfaceBright\">#3E373C</color>\n    <color name=\"md_purple_theme_surfaceContainerLowest\">#120D11</color>\n    <color name=\"md_purple_theme_surfaceContainerLow\">#1F1A1F</color>\n    <color name=\"md_purple_theme_surfaceContainer\">#241E23</color>\n    <color name=\"md_purple_theme_surfaceContainerHigh\">#2E282D</color>\n    <color name=\"md_purple_theme_surfaceContainerHighest\">#393338</color>\n    <color name=\"md_purple_theme_primary_mediumContrast\">#F1B8EF</color>\n    <color name=\"md_purple_theme_onPrimary_mediumContrast\">#2B0330</color>\n    <color name=\"md_purple_theme_primaryContainer_mediumContrast\">#B37FB3</color>\n    <color name=\"md_purple_theme_onPrimaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_purple_theme_secondary_mediumContrast\">#DDC3D8</color>\n    <color name=\"md_purple_theme_onSecondary_mediumContrast\">#201120</color>\n    <color name=\"md_purple_theme_secondaryContainer_mediumContrast\">#A08A9E</color>\n    <color name=\"md_purple_theme_onSecondaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_purple_theme_tertiary_mediumContrast\">#FABCB0</color>\n    <color name=\"md_purple_theme_onTertiary_mediumContrast\">#2C0C07</color>\n    <color name=\"md_purple_theme_tertiaryContainer_mediumContrast\">#BA8378</color>\n    <color name=\"md_purple_theme_onTertiaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_purple_theme_error_mediumContrast\">#FFBAB1</color>\n    <color name=\"md_purple_theme_onError_mediumContrast\">#370001</color>\n    <color name=\"md_purple_theme_errorContainer_mediumContrast\">#FF5449</color>\n    <color name=\"md_purple_theme_onErrorContainer_mediumContrast\">#000000</color>\n    <color name=\"md_purple_theme_background_mediumContrast\">#171216</color>\n    <color name=\"md_purple_theme_onBackground_mediumContrast\">#EBDFE6</color>\n    <color name=\"md_purple_theme_surface_mediumContrast\">#171216</color>\n    <color name=\"md_purple_theme_onSurface_mediumContrast\">#FFF9FA</color>\n    <color name=\"md_purple_theme_surfaceVariant_mediumContrast\">#4D444C</color>\n    <color name=\"md_purple_theme_onSurfaceVariant_mediumContrast\">#D4C7D0</color>\n    <color name=\"md_purple_theme_outline_mediumContrast\">#AC9FA8</color>\n    <color name=\"md_purple_theme_outlineVariant_mediumContrast\">#8B8088</color>\n    <color name=\"md_purple_theme_scrim_mediumContrast\">#000000</color>\n    <color name=\"md_purple_theme_inverseSurface_mediumContrast\">#EBDFE6</color>\n    <color name=\"md_purple_theme_inverseOnSurface_mediumContrast\">#2E282D</color>\n    <color name=\"md_purple_theme_inversePrimary_mediumContrast\">#643866</color>\n    <color name=\"md_purple_theme_primaryFixed_mediumContrast\">#FFD6FB</color>\n    <color name=\"md_purple_theme_onPrimaryFixed_mediumContrast\">#25002A</color>\n    <color name=\"md_purple_theme_primaryFixedDim_mediumContrast\">#EDB4EB</color>\n    <color name=\"md_purple_theme_onPrimaryFixedVariant_mediumContrast\">#502653</color>\n    <color name=\"md_purple_theme_secondaryFixed_mediumContrast\">#F5DBF1</color>\n    <color name=\"md_purple_theme_onSecondaryFixed_mediumContrast\">#1A0C1B</color>\n    <color name=\"md_purple_theme_secondaryFixedDim_mediumContrast\">#D8BFD4</color>\n    <color name=\"md_purple_theme_onSecondaryFixedVariant_mediumContrast\">#423141</color>\n    <color name=\"md_purple_theme_tertiaryFixed_mediumContrast\">#FFDAD3</color>\n    <color name=\"md_purple_theme_onTertiaryFixed_mediumContrast\">#250703</color>\n    <color name=\"md_purple_theme_tertiaryFixedDim_mediumContrast\">#F6B8AC</color>\n    <color name=\"md_purple_theme_onTertiaryFixedVariant_mediumContrast\">#532B23</color>\n    <color name=\"md_purple_theme_surfaceDim_mediumContrast\">#171216</color>\n    <color name=\"md_purple_theme_surfaceBright_mediumContrast\">#3E373C</color>\n    <color name=\"md_purple_theme_surfaceContainerLowest_mediumContrast\">#120D11</color>\n    <color name=\"md_purple_theme_surfaceContainerLow_mediumContrast\">#1F1A1F</color>\n    <color name=\"md_purple_theme_surfaceContainer_mediumContrast\">#241E23</color>\n    <color name=\"md_purple_theme_surfaceContainerHigh_mediumContrast\">#2E282D</color>\n    <color name=\"md_purple_theme_surfaceContainerHighest_mediumContrast\">#393338</color>\n    <color name=\"md_purple_theme_primary_highContrast\">#FFF9FA</color>\n    <color name=\"md_purple_theme_onPrimary_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_primaryContainer_highContrast\">#F1B8EF</color>\n    <color name=\"md_purple_theme_onPrimaryContainer_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_secondary_highContrast\">#FFF9FA</color>\n    <color name=\"md_purple_theme_onSecondary_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_secondaryContainer_highContrast\">#DDC3D8</color>\n    <color name=\"md_purple_theme_onSecondaryContainer_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_tertiary_highContrast\">#FFF9F8</color>\n    <color name=\"md_purple_theme_onTertiary_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_tertiaryContainer_highContrast\">#FABCB0</color>\n    <color name=\"md_purple_theme_onTertiaryContainer_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_error_highContrast\">#FFF9F9</color>\n    <color name=\"md_purple_theme_onError_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_errorContainer_highContrast\">#FFBAB1</color>\n    <color name=\"md_purple_theme_onErrorContainer_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_background_highContrast\">#171216</color>\n    <color name=\"md_purple_theme_onBackground_highContrast\">#EBDFE6</color>\n    <color name=\"md_purple_theme_surface_highContrast\">#171216</color>\n    <color name=\"md_purple_theme_onSurface_highContrast\">#FFFFFF</color>\n    <color name=\"md_purple_theme_surfaceVariant_highContrast\">#4D444C</color>\n    <color name=\"md_purple_theme_onSurfaceVariant_highContrast\">#FFF9FA</color>\n    <color name=\"md_purple_theme_outline_highContrast\">#D4C7D0</color>\n    <color name=\"md_purple_theme_outlineVariant_highContrast\">#D4C7D0</color>\n    <color name=\"md_purple_theme_scrim_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_inverseSurface_highContrast\">#EBDFE6</color>\n    <color name=\"md_purple_theme_inverseOnSurface_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_inversePrimary_highContrast\">#421946</color>\n    <color name=\"md_purple_theme_primaryFixed_highContrast\">#FFDCFB</color>\n    <color name=\"md_purple_theme_onPrimaryFixed_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_primaryFixedDim_highContrast\">#F1B8EF</color>\n    <color name=\"md_purple_theme_onPrimaryFixedVariant_highContrast\">#2B0330</color>\n    <color name=\"md_purple_theme_secondaryFixed_highContrast\">#FADFF5</color>\n    <color name=\"md_purple_theme_onSecondaryFixed_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_secondaryFixedDim_highContrast\">#DDC3D8</color>\n    <color name=\"md_purple_theme_onSecondaryFixedVariant_highContrast\">#201120</color>\n    <color name=\"md_purple_theme_tertiaryFixed_highContrast\">#FFE0DA</color>\n    <color name=\"md_purple_theme_onTertiaryFixed_highContrast\">#000000</color>\n    <color name=\"md_purple_theme_tertiaryFixedDim_highContrast\">#FABCB0</color>\n    <color name=\"md_purple_theme_onTertiaryFixedVariant_highContrast\">#2C0C07</color>\n    <color name=\"md_purple_theme_surfaceDim_highContrast\">#171216</color>\n    <color name=\"md_purple_theme_surfaceBright_highContrast\">#3E373C</color>\n    <color name=\"md_purple_theme_surfaceContainerLowest_highContrast\">#120D11</color>\n    <color name=\"md_purple_theme_surfaceContainerLow_highContrast\">#1F1A1F</color>\n    <color name=\"md_purple_theme_surfaceContainer_highContrast\">#241E23</color>\n    <color name=\"md_purple_theme_surfaceContainerHigh_highContrast\">#2E282D</color>\n    <color name=\"md_purple_theme_surfaceContainerHighest_highContrast\">#393338</color>\n\n    <!-- M3 Blue Dark Theme Colors -->\n    <color name=\"md_blue_theme_primary\">#A1CAFD</color>\n    <color name=\"md_blue_theme_onPrimary\">#003259</color>\n    <color name=\"md_blue_theme_primaryContainer\">#1A4975</color>\n    <color name=\"md_blue_theme_onPrimaryContainer\">#D2E4FF</color>\n    <color name=\"md_blue_theme_secondary\">#BBC7DB</color>\n    <color name=\"md_blue_theme_onSecondary\">#253140</color>\n    <color name=\"md_blue_theme_secondaryContainer\">#3C4858</color>\n    <color name=\"md_blue_theme_onSecondaryContainer\">#D7E3F8</color>\n    <color name=\"md_blue_theme_tertiary\">#D7BEE4</color>\n    <color name=\"md_blue_theme_onTertiary\">#3B2947</color>\n    <color name=\"md_blue_theme_tertiaryContainer\">#533F5F</color>\n    <color name=\"md_blue_theme_onTertiaryContainer\">#F3DAFF</color>\n    <color name=\"md_blue_theme_error\">#FFB4AB</color>\n    <color name=\"md_blue_theme_onError\">#690005</color>\n    <color name=\"md_blue_theme_errorContainer\">#93000A</color>\n    <color name=\"md_blue_theme_onErrorContainer\">#FFDAD6</color>\n    <color name=\"md_blue_theme_background\">#111418</color>\n    <color name=\"md_blue_theme_onBackground\">#E1E2E8</color>\n    <color name=\"md_blue_theme_surface\">#111418</color>\n    <color name=\"md_blue_theme_onSurface\">#E1E2E8</color>\n    <color name=\"md_blue_theme_surfaceVariant\">#43474E</color>\n    <color name=\"md_blue_theme_onSurfaceVariant\">#C3C6CF</color>\n    <color name=\"md_blue_theme_outline\">#8D9199</color>\n    <color name=\"md_blue_theme_outlineVariant\">#43474E</color>\n    <color name=\"md_blue_theme_scrim\">#000000</color>\n    <color name=\"md_blue_theme_inverseSurface\">#E1E2E8</color>\n    <color name=\"md_blue_theme_inverseOnSurface\">#2E3135</color>\n    <color name=\"md_blue_theme_inversePrimary\">#37618E</color>\n    <color name=\"md_blue_theme_primaryFixed\">#D2E4FF</color>\n    <color name=\"md_blue_theme_onPrimaryFixed\">#001D36</color>\n    <color name=\"md_blue_theme_primaryFixedDim\">#A1CAFD</color>\n    <color name=\"md_blue_theme_onPrimaryFixedVariant\">#1A4975</color>\n    <color name=\"md_blue_theme_secondaryFixed\">#D7E3F8</color>\n    <color name=\"md_blue_theme_onSecondaryFixed\">#101C2B</color>\n    <color name=\"md_blue_theme_secondaryFixedDim\">#BBC7DB</color>\n    <color name=\"md_blue_theme_onSecondaryFixedVariant\">#3C4858</color>\n    <color name=\"md_blue_theme_tertiaryFixed\">#F3DAFF</color>\n    <color name=\"md_blue_theme_onTertiaryFixed\">#251431</color>\n    <color name=\"md_blue_theme_tertiaryFixedDim\">#D7BEE4</color>\n    <color name=\"md_blue_theme_onTertiaryFixedVariant\">#533F5F</color>\n    <color name=\"md_blue_theme_surfaceDim\">#111418</color>\n    <color name=\"md_blue_theme_surfaceBright\">#36393E</color>\n    <color name=\"md_blue_theme_surfaceContainerLowest\">#0B0E13</color>\n    <color name=\"md_blue_theme_surfaceContainerLow\">#191C20</color>\n    <color name=\"md_blue_theme_surfaceContainer\">#1D2024</color>\n    <color name=\"md_blue_theme_surfaceContainerHigh\">#272A2F</color>\n    <color name=\"md_blue_theme_surfaceContainerHighest\">#32353A</color>\n    <color name=\"md_blue_theme_primary_mediumContrast\">#A7CEFF</color>\n    <color name=\"md_blue_theme_onPrimary_mediumContrast\">#00172E</color>\n    <color name=\"md_blue_theme_primaryContainer_mediumContrast\">#6B93C4</color>\n    <color name=\"md_blue_theme_onPrimaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_blue_theme_secondary_mediumContrast\">#BFCCDF</color>\n    <color name=\"md_blue_theme_onSecondary_mediumContrast\">#0A1725</color>\n    <color name=\"md_blue_theme_secondaryContainer_mediumContrast\">#8592A4</color>\n    <color name=\"md_blue_theme_onSecondaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_blue_theme_tertiary_mediumContrast\">#DBC2E8</color>\n    <color name=\"md_blue_theme_onTertiary_mediumContrast\">#200F2C</color>\n    <color name=\"md_blue_theme_tertiaryContainer_mediumContrast\">#9F88AC</color>\n    <color name=\"md_blue_theme_onTertiaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_blue_theme_error_mediumContrast\">#FFBAB1</color>\n    <color name=\"md_blue_theme_onError_mediumContrast\">#370001</color>\n    <color name=\"md_blue_theme_errorContainer_mediumContrast\">#FF5449</color>\n    <color name=\"md_blue_theme_onErrorContainer_mediumContrast\">#000000</color>\n    <color name=\"md_blue_theme_background_mediumContrast\">#111418</color>\n    <color name=\"md_blue_theme_onBackground_mediumContrast\">#E1E2E8</color>\n    <color name=\"md_blue_theme_surface_mediumContrast\">#111418</color>\n    <color name=\"md_blue_theme_onSurface_mediumContrast\">#FAFAFF</color>\n    <color name=\"md_blue_theme_surfaceVariant_mediumContrast\">#43474E</color>\n    <color name=\"md_blue_theme_onSurfaceVariant_mediumContrast\">#C7CBD3</color>\n    <color name=\"md_blue_theme_outline_mediumContrast\">#9FA3AB</color>\n    <color name=\"md_blue_theme_outlineVariant_mediumContrast\">#7F838B</color>\n    <color name=\"md_blue_theme_scrim_mediumContrast\">#000000</color>\n    <color name=\"md_blue_theme_inverseSurface_mediumContrast\">#E1E2E8</color>\n    <color name=\"md_blue_theme_inverseOnSurface_mediumContrast\">#272A2F</color>\n    <color name=\"md_blue_theme_inversePrimary_mediumContrast\">#1C4A76</color>\n    <color name=\"md_blue_theme_primaryFixed_mediumContrast\">#D2E4FF</color>\n    <color name=\"md_blue_theme_onPrimaryFixed_mediumContrast\">#001225</color>\n    <color name=\"md_blue_theme_primaryFixedDim_mediumContrast\">#A1CAFD</color>\n    <color name=\"md_blue_theme_onPrimaryFixedVariant_mediumContrast\">#003862</color>\n    <color name=\"md_blue_theme_secondaryFixed_mediumContrast\">#D7E3F8</color>\n    <color name=\"md_blue_theme_onSecondaryFixed_mediumContrast\">#051220</color>\n    <color name=\"md_blue_theme_secondaryFixedDim_mediumContrast\">#BBC7DB</color>\n    <color name=\"md_blue_theme_onSecondaryFixedVariant_mediumContrast\">#2B3746</color>\n    <color name=\"md_blue_theme_tertiaryFixed_mediumContrast\">#F3DAFF</color>\n    <color name=\"md_blue_theme_onTertiaryFixed_mediumContrast\">#1A0926</color>\n    <color name=\"md_blue_theme_tertiaryFixedDim_mediumContrast\">#D7BEE4</color>\n    <color name=\"md_blue_theme_onTertiaryFixedVariant_mediumContrast\">#412F4E</color>\n    <color name=\"md_blue_theme_surfaceDim_mediumContrast\">#111418</color>\n    <color name=\"md_blue_theme_surfaceBright_mediumContrast\">#36393E</color>\n    <color name=\"md_blue_theme_surfaceContainerLowest_mediumContrast\">#0B0E13</color>\n    <color name=\"md_blue_theme_surfaceContainerLow_mediumContrast\">#191C20</color>\n    <color name=\"md_blue_theme_surfaceContainer_mediumContrast\">#1D2024</color>\n    <color name=\"md_blue_theme_surfaceContainerHigh_mediumContrast\">#272A2F</color>\n    <color name=\"md_blue_theme_surfaceContainerHighest_mediumContrast\">#32353A</color>\n    <color name=\"md_blue_theme_primary_highContrast\">#FAFAFF</color>\n    <color name=\"md_blue_theme_onPrimary_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_primaryContainer_highContrast\">#A7CEFF</color>\n    <color name=\"md_blue_theme_onPrimaryContainer_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_secondary_highContrast\">#FAFAFF</color>\n    <color name=\"md_blue_theme_onSecondary_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_secondaryContainer_highContrast\">#BFCCDF</color>\n    <color name=\"md_blue_theme_onSecondaryContainer_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_tertiary_highContrast\">#FFF9FB</color>\n    <color name=\"md_blue_theme_onTertiary_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_tertiaryContainer_highContrast\">#DBC2E8</color>\n    <color name=\"md_blue_theme_onTertiaryContainer_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_error_highContrast\">#FFF9F9</color>\n    <color name=\"md_blue_theme_onError_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_errorContainer_highContrast\">#FFBAB1</color>\n    <color name=\"md_blue_theme_onErrorContainer_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_background_highContrast\">#111418</color>\n    <color name=\"md_blue_theme_onBackground_highContrast\">#E1E2E8</color>\n    <color name=\"md_blue_theme_surface_highContrast\">#111418</color>\n    <color name=\"md_blue_theme_onSurface_highContrast\">#FFFFFF</color>\n    <color name=\"md_blue_theme_surfaceVariant_highContrast\">#43474E</color>\n    <color name=\"md_blue_theme_onSurfaceVariant_highContrast\">#FAFAFF</color>\n    <color name=\"md_blue_theme_outline_highContrast\">#C7CBD3</color>\n    <color name=\"md_blue_theme_outlineVariant_highContrast\">#C7CBD3</color>\n    <color name=\"md_blue_theme_scrim_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_inverseSurface_highContrast\">#E1E2E8</color>\n    <color name=\"md_blue_theme_inverseOnSurface_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_inversePrimary_highContrast\">#002B4E</color>\n    <color name=\"md_blue_theme_primaryFixed_highContrast\">#D9E8FF</color>\n    <color name=\"md_blue_theme_onPrimaryFixed_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_primaryFixedDim_highContrast\">#A7CEFF</color>\n    <color name=\"md_blue_theme_onPrimaryFixedVariant_highContrast\">#00172E</color>\n    <color name=\"md_blue_theme_secondaryFixed_highContrast\">#DBE8FC</color>\n    <color name=\"md_blue_theme_onSecondaryFixed_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_secondaryFixedDim_highContrast\">#BFCCDF</color>\n    <color name=\"md_blue_theme_onSecondaryFixedVariant_highContrast\">#0A1725</color>\n    <color name=\"md_blue_theme_tertiaryFixed_highContrast\">#F5DFFF</color>\n    <color name=\"md_blue_theme_onTertiaryFixed_highContrast\">#000000</color>\n    <color name=\"md_blue_theme_tertiaryFixedDim_highContrast\">#DBC2E8</color>\n    <color name=\"md_blue_theme_onTertiaryFixedVariant_highContrast\">#200F2C</color>\n    <color name=\"md_blue_theme_surfaceDim_highContrast\">#111418</color>\n    <color name=\"md_blue_theme_surfaceBright_highContrast\">#36393E</color>\n    <color name=\"md_blue_theme_surfaceContainerLowest_highContrast\">#0B0E13</color>\n    <color name=\"md_blue_theme_surfaceContainerLow_highContrast\">#191C20</color>\n    <color name=\"md_blue_theme_surfaceContainer_highContrast\">#1D2024</color>\n    <color name=\"md_blue_theme_surfaceContainerHigh_highContrast\">#272A2F</color>\n    <color name=\"md_blue_theme_surfaceContainerHighest_highContrast\">#32353A</color>\n\n    <!-- M3 Green Dark Theme Colors -->\n    <color name=\"md_green_theme_primary\">#87D6BC</color>\n    <color name=\"md_green_theme_onPrimary\">#00382B</color>\n    <color name=\"md_green_theme_primaryContainer\">#005140</color>\n    <color name=\"md_green_theme_onPrimaryContainer\">#A3F2D8</color>\n    <color name=\"md_green_theme_secondary\">#B2CCC1</color>\n    <color name=\"md_green_theme_onSecondary\">#1D352D</color>\n    <color name=\"md_green_theme_secondaryContainer\">#344C43</color>\n    <color name=\"md_green_theme_onSecondaryContainer\">#CEE9DD</color>\n    <color name=\"md_green_theme_tertiary\">#A8CBE2</color>\n    <color name=\"md_green_theme_onTertiary\">#0D3446</color>\n    <color name=\"md_green_theme_tertiaryContainer\">#284B5E</color>\n    <color name=\"md_green_theme_onTertiaryContainer\">#C4E7FF</color>\n    <color name=\"md_green_theme_error\">#FFB4AB</color>\n    <color name=\"md_green_theme_onError\">#690005</color>\n    <color name=\"md_green_theme_errorContainer\">#93000A</color>\n    <color name=\"md_green_theme_onErrorContainer\">#FFDAD6</color>\n    <color name=\"md_green_theme_background\">#0F1512</color>\n    <color name=\"md_green_theme_onBackground\">#DEE4E0</color>\n    <color name=\"md_green_theme_surface\">#0F1512</color>\n    <color name=\"md_green_theme_onSurface\">#DEE4E0</color>\n    <color name=\"md_green_theme_surfaceVariant\">#3F4945</color>\n    <color name=\"md_green_theme_onSurfaceVariant\">#BFC9C3</color>\n    <color name=\"md_green_theme_outline\">#89938E</color>\n    <color name=\"md_green_theme_outlineVariant\">#3F4945</color>\n    <color name=\"md_green_theme_scrim\">#000000</color>\n    <color name=\"md_green_theme_inverseSurface\">#DEE4E0</color>\n    <color name=\"md_green_theme_inverseOnSurface\">#2B322F</color>\n    <color name=\"md_green_theme_inversePrimary\">#126B56</color>\n    <color name=\"md_green_theme_primaryFixed\">#A3F2D8</color>\n    <color name=\"md_green_theme_onPrimaryFixed\">#002018</color>\n    <color name=\"md_green_theme_primaryFixedDim\">#87D6BC</color>\n    <color name=\"md_green_theme_onPrimaryFixedVariant\">#005140</color>\n    <color name=\"md_green_theme_secondaryFixed\">#CEE9DD</color>\n    <color name=\"md_green_theme_onSecondaryFixed\">#072019</color>\n    <color name=\"md_green_theme_secondaryFixedDim\">#B2CCC1</color>\n    <color name=\"md_green_theme_onSecondaryFixedVariant\">#344C43</color>\n    <color name=\"md_green_theme_tertiaryFixed\">#C4E7FF</color>\n    <color name=\"md_green_theme_onTertiaryFixed\">#001E2C</color>\n    <color name=\"md_green_theme_tertiaryFixedDim\">#A8CBE2</color>\n    <color name=\"md_green_theme_onTertiaryFixedVariant\">#284B5E</color>\n    <color name=\"md_green_theme_surfaceDim\">#0F1512</color>\n    <color name=\"md_green_theme_surfaceBright\">#343B38</color>\n    <color name=\"md_green_theme_surfaceContainerLowest\">#090F0D</color>\n    <color name=\"md_green_theme_surfaceContainerLow\">#171D1A</color>\n    <color name=\"md_green_theme_surfaceContainer\">#1B211E</color>\n    <color name=\"md_green_theme_surfaceContainerHigh\">#252B29</color>\n    <color name=\"md_green_theme_surfaceContainerHighest\">#303633</color>\n    <color name=\"md_green_theme_primary_mediumContrast\">#8BDAC0</color>\n    <color name=\"md_green_theme_onPrimary_mediumContrast\">#001B13</color>\n    <color name=\"md_green_theme_primaryContainer_mediumContrast\">#509F87</color>\n    <color name=\"md_green_theme_onPrimaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_green_theme_secondary_mediumContrast\">#B6D1C6</color>\n    <color name=\"md_green_theme_onSecondary_mediumContrast\">#031A14</color>\n    <color name=\"md_green_theme_secondaryContainer_mediumContrast\">#7D968C</color>\n    <color name=\"md_green_theme_onSecondaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_green_theme_tertiary_mediumContrast\">#ACD0E6</color>\n    <color name=\"md_green_theme_onTertiary_mediumContrast\">#001925</color>\n    <color name=\"md_green_theme_tertiaryContainer_mediumContrast\">#7395AB</color>\n    <color name=\"md_green_theme_onTertiaryContainer_mediumContrast\">#000000</color>\n    <color name=\"md_green_theme_error_mediumContrast\">#FFBAB1</color>\n    <color name=\"md_green_theme_onError_mediumContrast\">#370001</color>\n    <color name=\"md_green_theme_errorContainer_mediumContrast\">#FF5449</color>\n    <color name=\"md_green_theme_onErrorContainer_mediumContrast\">#000000</color>\n    <color name=\"md_green_theme_background_mediumContrast\">#0F1512</color>\n    <color name=\"md_green_theme_onBackground_mediumContrast\">#DEE4E0</color>\n    <color name=\"md_green_theme_surface_mediumContrast\">#0F1512</color>\n    <color name=\"md_green_theme_onSurface_mediumContrast\">#F6FCF8</color>\n    <color name=\"md_green_theme_surfaceVariant_mediumContrast\">#3F4945</color>\n    <color name=\"md_green_theme_onSurfaceVariant_mediumContrast\">#C3CDC8</color>\n    <color name=\"md_green_theme_outline_mediumContrast\">#9BA5A0</color>\n    <color name=\"md_green_theme_outlineVariant_mediumContrast\">#7B8581</color>\n    <color name=\"md_green_theme_scrim_mediumContrast\">#000000</color>\n    <color name=\"md_green_theme_inverseSurface_mediumContrast\">#DEE4E0</color>\n    <color name=\"md_green_theme_inverseOnSurface_mediumContrast\">#252B29</color>\n    <color name=\"md_green_theme_inversePrimary_mediumContrast\">#005241</color>\n    <color name=\"md_green_theme_primaryFixed_mediumContrast\">#A3F2D8</color>\n    <color name=\"md_green_theme_onPrimaryFixed_mediumContrast\">#00150F</color>\n    <color name=\"md_green_theme_primaryFixedDim_mediumContrast\">#87D6BC</color>\n    <color name=\"md_green_theme_onPrimaryFixedVariant_mediumContrast\">#003E31</color>\n    <color name=\"md_green_theme_secondaryFixed_mediumContrast\">#CEE9DD</color>\n    <color name=\"md_green_theme_onSecondaryFixed_mediumContrast\">#00150F</color>\n    <color name=\"md_green_theme_secondaryFixedDim_mediumContrast\">#B2CCC1</color>\n    <color name=\"md_green_theme_onSecondaryFixedVariant_mediumContrast\">#233B33</color>\n    <color name=\"md_green_theme_tertiaryFixed_mediumContrast\">#C4E7FF</color>\n    <color name=\"md_green_theme_onTertiaryFixed_mediumContrast\">#00131E</color>\n    <color name=\"md_green_theme_tertiaryFixedDim_mediumContrast\">#A8CBE2</color>\n    <color name=\"md_green_theme_onTertiaryFixedVariant_mediumContrast\">#153A4C</color>\n    <color name=\"md_green_theme_surfaceDim_mediumContrast\">#0F1512</color>\n    <color name=\"md_green_theme_surfaceBright_mediumContrast\">#343B38</color>\n    <color name=\"md_green_theme_surfaceContainerLowest_mediumContrast\">#090F0D</color>\n    <color name=\"md_green_theme_surfaceContainerLow_mediumContrast\">#171D1A</color>\n    <color name=\"md_green_theme_surfaceContainer_mediumContrast\">#1B211E</color>\n    <color name=\"md_green_theme_surfaceContainerHigh_mediumContrast\">#252B29</color>\n    <color name=\"md_green_theme_surfaceContainerHighest_mediumContrast\">#303633</color>\n    <color name=\"md_green_theme_primary_highContrast\">#EDFFF6</color>\n    <color name=\"md_green_theme_onPrimary_highContrast\">#000000</color>\n    <color name=\"md_green_theme_primaryContainer_highContrast\">#8BDAC0</color>\n    <color name=\"md_green_theme_onPrimaryContainer_highContrast\">#000000</color>\n    <color name=\"md_green_theme_secondary_highContrast\">#EDFFF6</color>\n    <color name=\"md_green_theme_onSecondary_highContrast\">#000000</color>\n    <color name=\"md_green_theme_secondaryContainer_highContrast\">#B6D1C6</color>\n    <color name=\"md_green_theme_onSecondaryContainer_highContrast\">#000000</color>\n    <color name=\"md_green_theme_tertiary_highContrast\">#F8FBFF</color>\n    <color name=\"md_green_theme_onTertiary_highContrast\">#000000</color>\n    <color name=\"md_green_theme_tertiaryContainer_highContrast\">#ACD0E6</color>\n    <color name=\"md_green_theme_onTertiaryContainer_highContrast\">#000000</color>\n    <color name=\"md_green_theme_error_highContrast\">#FFF9F9</color>\n    <color name=\"md_green_theme_onError_highContrast\">#000000</color>\n    <color name=\"md_green_theme_errorContainer_highContrast\">#FFBAB1</color>\n    <color name=\"md_green_theme_onErrorContainer_highContrast\">#000000</color>\n    <color name=\"md_green_theme_background_highContrast\">#0F1512</color>\n    <color name=\"md_green_theme_onBackground_highContrast\">#DEE4E0</color>\n    <color name=\"md_green_theme_surface_highContrast\">#0F1512</color>\n    <color name=\"md_green_theme_onSurface_highContrast\">#FFFFFF</color>\n    <color name=\"md_green_theme_surfaceVariant_highContrast\">#3F4945</color>\n    <color name=\"md_green_theme_onSurfaceVariant_highContrast\">#F3FDF7</color>\n    <color name=\"md_green_theme_outline_highContrast\">#C3CDC8</color>\n    <color name=\"md_green_theme_outlineVariant_highContrast\">#C3CDC8</color>\n    <color name=\"md_green_theme_scrim_highContrast\">#000000</color>\n    <color name=\"md_green_theme_inverseSurface_highContrast\">#DEE4E0</color>\n    <color name=\"md_green_theme_inverseOnSurface_highContrast\">#000000</color>\n    <color name=\"md_green_theme_inversePrimary_highContrast\">#003126</color>\n    <color name=\"md_green_theme_primaryFixed_highContrast\">#A7F7DC</color>\n    <color name=\"md_green_theme_onPrimaryFixed_highContrast\">#000000</color>\n    <color name=\"md_green_theme_primaryFixedDim_highContrast\">#8BDAC0</color>\n    <color name=\"md_green_theme_onPrimaryFixedVariant_highContrast\">#001B13</color>\n    <color name=\"md_green_theme_secondaryFixed_highContrast\">#D2EDE1</color>\n    <color name=\"md_green_theme_onSecondaryFixed_highContrast\">#000000</color>\n    <color name=\"md_green_theme_secondaryFixedDim_highContrast\">#B6D1C6</color>\n    <color name=\"md_green_theme_onSecondaryFixedVariant_highContrast\">#031A14</color>\n    <color name=\"md_green_theme_tertiaryFixed_highContrast\">#CDEBFF</color>\n    <color name=\"md_green_theme_onTertiaryFixed_highContrast\">#000000</color>\n    <color name=\"md_green_theme_tertiaryFixedDim_highContrast\">#ACD0E6</color>\n    <color name=\"md_green_theme_onTertiaryFixedVariant_highContrast\">#001925</color>\n    <color name=\"md_green_theme_surfaceDim_highContrast\">#0F1512</color>\n    <color name=\"md_green_theme_surfaceBright_highContrast\">#343B38</color>\n    <color name=\"md_green_theme_surfaceContainerLowest_highContrast\">#090F0D</color>\n    <color name=\"md_green_theme_surfaceContainerLow_highContrast\">#171D1A</color>\n    <color name=\"md_green_theme_surfaceContainer_highContrast\">#1B211E</color>\n    <color name=\"md_green_theme_surfaceContainerHigh_highContrast\">#252B29</color>\n    <color name=\"md_green_theme_surfaceContainerHighest_highContrast\">#303633</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-night/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <!-- Base application theme. -->\n    <style name=\"Theme.Kitsune.DayNight\" parent=\"Theme.Kitsune\">\n        <item name=\"colorPrimary\">@color/md_theme_primary</item>\n        <item name=\"colorOnPrimary\">@color/md_theme_onPrimary</item>\n        <item name=\"colorPrimaryContainer\">@color/md_theme_primaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/md_theme_onPrimaryContainer</item>\n        <item name=\"colorSecondary\">@color/md_theme_secondary</item>\n        <item name=\"colorOnSecondary\">@color/md_theme_onSecondary</item>\n        <item name=\"colorSecondaryContainer\">@color/md_theme_secondaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/md_theme_onSecondaryContainer</item>\n        <item name=\"colorTertiary\">@color/md_theme_tertiary</item>\n        <item name=\"colorOnTertiary\">@color/md_theme_onTertiary</item>\n        <item name=\"colorTertiaryContainer\">@color/md_theme_tertiaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/md_theme_onTertiaryContainer</item>\n        <item name=\"colorError\">@color/md_theme_error</item>\n        <item name=\"colorOnError\">@color/md_theme_onError</item>\n        <item name=\"colorErrorContainer\">@color/md_theme_errorContainer</item>\n        <item name=\"colorOnErrorContainer\">@color/md_theme_onErrorContainer</item>\n        <item name=\"android:colorBackground\">@color/md_theme_background</item>\n        <item name=\"colorOnBackground\">@color/md_theme_onBackground</item>\n        <item name=\"colorSurface\">@color/md_theme_surface</item>\n        <item name=\"colorOnSurface\">@color/md_theme_onSurface</item>\n        <item name=\"colorSurfaceVariant\">@color/md_theme_surfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/md_theme_onSurfaceVariant</item>\n        <item name=\"colorOutline\">@color/md_theme_outline</item>\n        <item name=\"colorOutlineVariant\">@color/md_theme_outlineVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/md_theme_inverseSurface</item>\n        <item name=\"colorOnSurfaceInverse\">@color/md_theme_inverseOnSurface</item>\n        <item name=\"colorPrimaryInverse\">@color/md_theme_inversePrimary</item>\n        <item name=\"colorPrimaryFixed\">@color/md_theme_primaryFixed</item>\n        <item name=\"colorOnPrimaryFixed\">@color/md_theme_onPrimaryFixed</item>\n        <item name=\"colorPrimaryFixedDim\">@color/md_theme_primaryFixedDim</item>\n        <item name=\"colorOnPrimaryFixedVariant\">@color/md_theme_onPrimaryFixedVariant</item>\n        <item name=\"colorSecondaryFixed\">@color/md_theme_secondaryFixed</item>\n        <item name=\"colorOnSecondaryFixed\">@color/md_theme_onSecondaryFixed</item>\n        <item name=\"colorSecondaryFixedDim\">@color/md_theme_secondaryFixedDim</item>\n        <item name=\"colorOnSecondaryFixedVariant\">@color/md_theme_onSecondaryFixedVariant</item>\n        <item name=\"colorTertiaryFixed\">@color/md_theme_tertiaryFixed</item>\n        <item name=\"colorOnTertiaryFixed\">@color/md_theme_onTertiaryFixed</item>\n        <item name=\"colorTertiaryFixedDim\">@color/md_theme_tertiaryFixedDim</item>\n        <item name=\"colorOnTertiaryFixedVariant\">@color/md_theme_onTertiaryFixedVariant</item>\n        <item name=\"colorSurfaceDim\">@color/md_theme_surfaceDim</item>\n        <item name=\"colorSurfaceBright\">@color/md_theme_surfaceBright</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/md_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/md_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/md_theme_surfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_theme_surfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_theme_surfaceContainerHighest</item>\n\n        <item name=\"android:windowLightStatusBar\">false</item>\n        <item name=\"android:windowLightNavigationBar\" tools:targetApi=\"o_mr1\">false</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Purple\" parent=\"Theme.Kitsune.DayNight\">\n        <item name=\"colorPrimary\">@color/md_purple_theme_primary</item>\n        <item name=\"colorOnPrimary\">@color/md_purple_theme_onPrimary</item>\n        <item name=\"colorPrimaryContainer\">@color/md_purple_theme_primaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/md_purple_theme_onPrimaryContainer</item>\n        <item name=\"colorSecondary\">@color/md_purple_theme_secondary</item>\n        <item name=\"colorOnSecondary\">@color/md_purple_theme_onSecondary</item>\n        <item name=\"colorSecondaryContainer\">@color/md_purple_theme_secondaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/md_purple_theme_onSecondaryContainer</item>\n        <item name=\"colorTertiary\">@color/md_purple_theme_tertiary</item>\n        <item name=\"colorOnTertiary\">@color/md_purple_theme_onTertiary</item>\n        <item name=\"colorTertiaryContainer\">@color/md_purple_theme_tertiaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/md_purple_theme_onTertiaryContainer</item>\n        <item name=\"colorError\">@color/md_purple_theme_error</item>\n        <item name=\"colorOnError\">@color/md_purple_theme_onError</item>\n        <item name=\"colorErrorContainer\">@color/md_purple_theme_errorContainer</item>\n        <item name=\"colorOnErrorContainer\">@color/md_purple_theme_onErrorContainer</item>\n        <item name=\"android:colorBackground\">@color/md_purple_theme_background</item>\n        <item name=\"colorOnBackground\">@color/md_purple_theme_onBackground</item>\n        <item name=\"colorSurface\">@color/md_purple_theme_surface</item>\n        <item name=\"colorOnSurface\">@color/md_purple_theme_onSurface</item>\n        <item name=\"colorSurfaceVariant\">@color/md_purple_theme_surfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/md_purple_theme_onSurfaceVariant</item>\n        <item name=\"colorOutline\">@color/md_purple_theme_outline</item>\n        <item name=\"colorOutlineVariant\">@color/md_purple_theme_outlineVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/md_purple_theme_inverseSurface</item>\n        <item name=\"colorOnSurfaceInverse\">@color/md_purple_theme_inverseOnSurface</item>\n        <item name=\"colorPrimaryInverse\">@color/md_purple_theme_inversePrimary</item>\n        <item name=\"colorPrimaryFixed\">@color/md_purple_theme_primaryFixed</item>\n        <item name=\"colorOnPrimaryFixed\">@color/md_purple_theme_onPrimaryFixed</item>\n        <item name=\"colorPrimaryFixedDim\">@color/md_purple_theme_primaryFixedDim</item>\n        <item name=\"colorOnPrimaryFixedVariant\">@color/md_purple_theme_onPrimaryFixedVariant</item>\n        <item name=\"colorSecondaryFixed\">@color/md_purple_theme_secondaryFixed</item>\n        <item name=\"colorOnSecondaryFixed\">@color/md_purple_theme_onSecondaryFixed</item>\n        <item name=\"colorSecondaryFixedDim\">@color/md_purple_theme_secondaryFixedDim</item>\n        <item name=\"colorOnSecondaryFixedVariant\">@color/md_purple_theme_onSecondaryFixedVariant</item>\n        <item name=\"colorTertiaryFixed\">@color/md_purple_theme_tertiaryFixed</item>\n        <item name=\"colorOnTertiaryFixed\">@color/md_purple_theme_onTertiaryFixed</item>\n        <item name=\"colorTertiaryFixedDim\">@color/md_purple_theme_tertiaryFixedDim</item>\n        <item name=\"colorOnTertiaryFixedVariant\">@color/md_purple_theme_onTertiaryFixedVariant</item>\n        <item name=\"colorSurfaceDim\">@color/md_purple_theme_surfaceDim</item>\n        <item name=\"colorSurfaceBright\">@color/md_purple_theme_surfaceBright</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/md_purple_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/md_purple_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/md_purple_theme_surfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_purple_theme_surfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_purple_theme_surfaceContainerHighest</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Purple</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Blue\" parent=\"Theme.Kitsune.DayNight\">\n        <item name=\"colorPrimary\">@color/md_blue_theme_primary</item>\n        <item name=\"colorOnPrimary\">@color/md_blue_theme_onPrimary</item>\n        <item name=\"colorPrimaryContainer\">@color/md_blue_theme_primaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/md_blue_theme_onPrimaryContainer</item>\n        <item name=\"colorSecondary\">@color/md_blue_theme_secondary</item>\n        <item name=\"colorOnSecondary\">@color/md_blue_theme_onSecondary</item>\n        <item name=\"colorSecondaryContainer\">@color/md_blue_theme_secondaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/md_blue_theme_onSecondaryContainer</item>\n        <item name=\"colorTertiary\">@color/md_blue_theme_tertiary</item>\n        <item name=\"colorOnTertiary\">@color/md_blue_theme_onTertiary</item>\n        <item name=\"colorTertiaryContainer\">@color/md_blue_theme_tertiaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/md_blue_theme_onTertiaryContainer</item>\n        <item name=\"colorError\">@color/md_blue_theme_error</item>\n        <item name=\"colorOnError\">@color/md_blue_theme_onError</item>\n        <item name=\"colorErrorContainer\">@color/md_blue_theme_errorContainer</item>\n        <item name=\"colorOnErrorContainer\">@color/md_blue_theme_onErrorContainer</item>\n        <item name=\"android:colorBackground\">@color/md_blue_theme_background</item>\n        <item name=\"colorOnBackground\">@color/md_blue_theme_onBackground</item>\n        <item name=\"colorSurface\">@color/md_blue_theme_surface</item>\n        <item name=\"colorOnSurface\">@color/md_blue_theme_onSurface</item>\n        <item name=\"colorSurfaceVariant\">@color/md_blue_theme_surfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/md_blue_theme_onSurfaceVariant</item>\n        <item name=\"colorOutline\">@color/md_blue_theme_outline</item>\n        <item name=\"colorOutlineVariant\">@color/md_blue_theme_outlineVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/md_blue_theme_inverseSurface</item>\n        <item name=\"colorOnSurfaceInverse\">@color/md_blue_theme_inverseOnSurface</item>\n        <item name=\"colorPrimaryInverse\">@color/md_blue_theme_inversePrimary</item>\n        <item name=\"colorPrimaryFixed\">@color/md_blue_theme_primaryFixed</item>\n        <item name=\"colorOnPrimaryFixed\">@color/md_blue_theme_onPrimaryFixed</item>\n        <item name=\"colorPrimaryFixedDim\">@color/md_blue_theme_primaryFixedDim</item>\n        <item name=\"colorOnPrimaryFixedVariant\">@color/md_blue_theme_onPrimaryFixedVariant</item>\n        <item name=\"colorSecondaryFixed\">@color/md_blue_theme_secondaryFixed</item>\n        <item name=\"colorOnSecondaryFixed\">@color/md_blue_theme_onSecondaryFixed</item>\n        <item name=\"colorSecondaryFixedDim\">@color/md_blue_theme_secondaryFixedDim</item>\n        <item name=\"colorOnSecondaryFixedVariant\">@color/md_blue_theme_onSecondaryFixedVariant</item>\n        <item name=\"colorTertiaryFixed\">@color/md_blue_theme_tertiaryFixed</item>\n        <item name=\"colorOnTertiaryFixed\">@color/md_blue_theme_onTertiaryFixed</item>\n        <item name=\"colorTertiaryFixedDim\">@color/md_blue_theme_tertiaryFixedDim</item>\n        <item name=\"colorOnTertiaryFixedVariant\">@color/md_blue_theme_onTertiaryFixedVariant</item>\n        <item name=\"colorSurfaceDim\">@color/md_blue_theme_surfaceDim</item>\n        <item name=\"colorSurfaceBright\">@color/md_blue_theme_surfaceBright</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/md_blue_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/md_blue_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/md_blue_theme_surfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_blue_theme_surfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_blue_theme_surfaceContainerHighest</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Blue</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Green\" parent=\"Theme.Kitsune.DayNight\">\n        <item name=\"colorPrimary\">@color/md_green_theme_primary</item>\n        <item name=\"colorOnPrimary\">@color/md_green_theme_onPrimary</item>\n        <item name=\"colorPrimaryContainer\">@color/md_green_theme_primaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/md_green_theme_onPrimaryContainer</item>\n        <item name=\"colorSecondary\">@color/md_green_theme_secondary</item>\n        <item name=\"colorOnSecondary\">@color/md_green_theme_onSecondary</item>\n        <item name=\"colorSecondaryContainer\">@color/md_green_theme_secondaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/md_green_theme_onSecondaryContainer</item>\n        <item name=\"colorTertiary\">@color/md_green_theme_tertiary</item>\n        <item name=\"colorOnTertiary\">@color/md_green_theme_onTertiary</item>\n        <item name=\"colorTertiaryContainer\">@color/md_green_theme_tertiaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/md_green_theme_onTertiaryContainer</item>\n        <item name=\"colorError\">@color/md_green_theme_error</item>\n        <item name=\"colorOnError\">@color/md_green_theme_onError</item>\n        <item name=\"colorErrorContainer\">@color/md_green_theme_errorContainer</item>\n        <item name=\"colorOnErrorContainer\">@color/md_green_theme_onErrorContainer</item>\n        <item name=\"android:colorBackground\">@color/md_green_theme_background</item>\n        <item name=\"colorOnBackground\">@color/md_green_theme_onBackground</item>\n        <item name=\"colorSurface\">@color/md_green_theme_surface</item>\n        <item name=\"colorOnSurface\">@color/md_green_theme_onSurface</item>\n        <item name=\"colorSurfaceVariant\">@color/md_green_theme_surfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/md_green_theme_onSurfaceVariant</item>\n        <item name=\"colorOutline\">@color/md_green_theme_outline</item>\n        <item name=\"colorOutlineVariant\">@color/md_green_theme_outlineVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/md_green_theme_inverseSurface</item>\n        <item name=\"colorOnSurfaceInverse\">@color/md_green_theme_inverseOnSurface</item>\n        <item name=\"colorPrimaryInverse\">@color/md_green_theme_inversePrimary</item>\n        <item name=\"colorPrimaryFixed\">@color/md_green_theme_primaryFixed</item>\n        <item name=\"colorOnPrimaryFixed\">@color/md_green_theme_onPrimaryFixed</item>\n        <item name=\"colorPrimaryFixedDim\">@color/md_green_theme_primaryFixedDim</item>\n        <item name=\"colorOnPrimaryFixedVariant\">@color/md_green_theme_onPrimaryFixedVariant</item>\n        <item name=\"colorSecondaryFixed\">@color/md_green_theme_secondaryFixed</item>\n        <item name=\"colorOnSecondaryFixed\">@color/md_green_theme_onSecondaryFixed</item>\n        <item name=\"colorSecondaryFixedDim\">@color/md_green_theme_secondaryFixedDim</item>\n        <item name=\"colorOnSecondaryFixedVariant\">@color/md_green_theme_onSecondaryFixedVariant</item>\n        <item name=\"colorTertiaryFixed\">@color/md_green_theme_tertiaryFixed</item>\n        <item name=\"colorOnTertiaryFixed\">@color/md_green_theme_onTertiaryFixed</item>\n        <item name=\"colorTertiaryFixedDim\">@color/md_green_theme_tertiaryFixedDim</item>\n        <item name=\"colorOnTertiaryFixedVariant\">@color/md_green_theme_onTertiaryFixedVariant</item>\n        <item name=\"colorSurfaceDim\">@color/md_green_theme_surfaceDim</item>\n        <item name=\"colorSurfaceBright\">@color/md_green_theme_surfaceBright</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/md_green_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/md_green_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/md_green_theme_surfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_green_theme_surfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_green_theme_surfaceContainerHighest</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Green</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Black\">\n        <item name=\"android:colorBackground\">@color/black_background</item>\n        <item name=\"colorSurface\">@color/black_background</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/black_background</item>\n        <item name=\"colorSurfaceContainerLow\">@color/black_background</item>\n        <item name=\"colorSurfaceContainer\">@color/md_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_theme_surfaceContainer</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Black</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Purple.Black\">\n        <item name=\"android:colorBackground\">@color/black_background</item>\n        <item name=\"colorSurface\">@color/black_background</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/black_background</item>\n        <item name=\"colorSurfaceContainerLow\">@color/black_background</item>\n        <item name=\"colorSurfaceContainer\">@color/md_purple_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_purple_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_purple_theme_surfaceContainer</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Purple.Black</item>\n    </style>\n    \n    <style name=\"Theme.Kitsune.DayNight.Blue.Black\">\n        <item name=\"android:colorBackground\">@color/black_background</item>\n        <item name=\"colorSurface\">@color/black_background</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/black_background</item>\n        <item name=\"colorSurfaceContainerLow\">@color/black_background</item>\n        <item name=\"colorSurfaceContainer\">@color/md_blue_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_blue_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_blue_theme_surfaceContainer</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Blue.Black</item>\n    </style>\n\n    <style name=\"Theme.Kitsune.DayNight.Green.Black\">\n        <item name=\"android:colorBackground\">@color/black_background</item>\n        <item name=\"colorSurface\">@color/black_background</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/black_background</item>\n        <item name=\"colorSurfaceContainerLow\">@color/black_background</item>\n        <item name=\"colorSurfaceContainer\">@color/md_green_theme_surfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/md_green_theme_surfaceContainerLow</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/md_green_theme_surfaceContainer</item>\n\n        <item name=\"fullScreenDialogTheme\">@style/Theme.Kitsune.FullScreenDialog.Green.Black</item>\n    </style>\n\n    <!-- SplashScreen theme -->\n    <style name=\"Theme.Kitsune.SplashScreen.DayNight\" parent=\"Theme.Kitsune.SplashScreen\">\n        <item name=\"windowSplashScreenBackground\">@color/black</item>\n        <item name=\"android:windowLightStatusBar\">false</item>\n        <item name=\"android:windowLightNavigationBar\" tools:targetApi=\"o_mr1\">false</item>\n    </style>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values-pt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"nav_home\">Início</string>\n    <string name=\"nav_search\">Pesquisar</string>\n    <string name=\"nav_library\">Biblioteca</string>\n    <string name=\"nav_profile\">Perfil</string>\n    <string name=\"nav_settings\">Configurações</string>\n    <string name=\"nav_appearance\">Aparência</string>\n    <string name=\"nav_app_logs\">Registos da aplicação</string>\n    <string name=\"nav_os_libraries\">Bibliotecas de código aberto</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"manga\">Mangá</string>\n    <string name=\"action_retry\">Tentar novamente</string>\n    <string name=\"action_ok\">OK</string>\n    <string name=\"action_cancel\">Cancelar</string>\n    <string name=\"action_reset_filter\">Redefinir filtro</string>\n    <string name=\"action_unselect_all\">Desmarque todos</string>\n    <string name=\"action_read_more\">Ler mais</string>\n    <string name=\"action_read_less\">Ler menos</string>\n    <string name=\"action_show_more\">Mostrar mais</string>\n    <string name=\"action_show_less\">Mostrar menos</string>\n    <string name=\"action_view\">Visualizar</string>\n    <string name=\"action_back\">Voltar</string>\n    <string name=\"action_skip\">Pular</string>\n    <string name=\"action_next\">Próximo</string>\n    <string name=\"action_continue\">Continuar</string>\n    <string name=\"action_close\">Fechar</string>\n    <string name=\"action_log_out\">Deslogar</string>\n    <string name=\"action_share_profile_url\">Partilhar Perfil</string>\n    <string name=\"action_share\">Partilhar</string>\n    <string name=\"action_open_external\">Abrir num site externo</string>\n    <string name=\"action_open_external_short\">Sites externos</string>\n    <string name=\"action_share_app_logs\">Partilhar registos</string>\n    <string name=\"action_add_to_favorites\">Adicionar aos favoritos</string>\n    <string name=\"action_remove_from_favorites\">Remover dos favoritos</string>\n    <string name=\"action_dismiss\">Descartar</string>\n    <string name=\"action_synchronize\">Sincronizar</string>\n    <string name=\"action_save_in_gallery\">Gravar na galeria</string>\n    <string name=\"action_open_in_browser\">Abrir no navegador</string>\n    <string name=\"action_open_on_mal\">Abrir no MyAnimeList.net</string>\n    <string name=\"action_save_changes\">Gravar mudanças</string>\n    <string name=\"action_edit_profile\">Editar perfil</string>\n    <string name=\"action_update_profile\">Atualizar perfil</string>\n    <string name=\"action_add\">Adicionar</string>\n    <string name=\"action_remove\">Remover</string>\n    <string name=\"action_update\">Atualizar</string>\n    <string name=\"action_start\">Começar</string>\n    <string name=\"action_allow\">Permitir</string>\n    <string name=\"action_rate\">Avaliar</string>\n    <string name=\"action_update_rating\">Atualizar avaliação</string>\n    <string name=\"action_remove_rating\">Remover avaliação</string>\n    <string name=\"action_add_profile_link\">Adicionar ligação ao perfil</string>\n    <string name=\"action_edit_profile_link\">Editar ligação ao perfil</string>\n    <string name=\"action_select_profile_link_site\">Selecionar um site</string>\n    <string name=\"action_delete_profile_link\">Remover ligação ao perfil</string>\n    <string name=\"hint_search\">Pesquisar</string>\n    <string name=\"hint_search_characters\">Pesquisar personagens</string>\n    <string name=\"hint_mark_watched\">Marcar como assistido.</string>\n    <string name=\"hint_mark_not_watched\">Marcar como não assistido.</string>\n    <string name=\"hint_rating\">Avaliar</string>\n    <string name=\"dialog_log_out_confirmation\">Tem certeza que quer deslogar?</string>\n    <string name=\"dialog_remove_from_library_title\">Remover da biblioteca?</string>\n    <string name=\"dialog_remove_from_library_msg\"><b>%s</b> vai ser removido da sua biblioteca.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Permissão rejeitada</string>\n    <string name=\"dialog_notification_permission_rejected\">Não vai receber notificações e nem será informado sobre atualizações da app.</string>\n    <string name=\"dialog_request_notification_permission_title\">Permitir notificações</string>\n    <string name=\"dialog_request_notification_permission\">Permitir ser notificado quando uma nova atualização estiver disponível.</string>\n    <string name=\"error_resource_loading\">Não foi possível carregar os dados.</string>\n    <string name=\"error_image_loading\">Falha ao carregar a imagens.</string>\n    <string name=\"error_mapping_loading\">Falha ao carregar mapeamento de sites externos.</string>\n    <string name=\"error_nothing_found\">Não encontrado.</string>\n    <string name=\"error_invalid_user\">Utilizador Inválido. Está logado?</string>\n    <string name=\"error_user_update_failed\">Falha ao atualizar os dados do utilizador.</string>\n    <string name=\"error_user_delete_waifu_failed\">Falha ao remover Waifu/Husbando.</string>\n    <string name=\"error_user_update_image_failed\">Falha ao atualizar a imagem de perfil ou capa.</string>\n    <string name=\"error_user_create_profile_link_failed\">Falha ao criar a ligação ao perfil do %s.</string>\n    <string name=\"error_user_update_profile_link_failed\">Falha ao atualizar a ligação ao perfil do %s.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Falha ao apagar a ligação ao perfil do %s.</string>\n    <string name=\"error_something_wrong\">Ena, algo deu errado!</string>\n    <string name=\"error_requires_external_storage_permission\">Requer permissão para gravar no armazenamento externo.</string>\n    <string name=\"error_search_provider_unavailable\">Provedor de pesquisa indisponível.</string>\n    <string name=\"error_library_update_failed\">Falha ao atualizar a entrada na biblioteca.</string>\n    <string name=\"error_library_update_failed_multiple\">Falha ao atualizar %d entradas na biblioteca.</string>\n    <string name=\"error_library_update_not_found\">A entrada na biblioteca não existe.</string>\n    <string name=\"error_library_add_failed\">Falha ao adicionar à sua biblioteca.</string>\n    <string name=\"error_library_delete_failed\">Falha ao remover entrada da biblioteca.</string>\n    <string name=\"error_requires_notification_permission\">Permissão as notificações é necessária.</string>\n    <string name=\"sort_popularity_asc\">Menos popular</string>\n    <string name=\"sort_popularity_desc\">Mais popular</string>\n    <string name=\"sort_average_rating_asc\">Pior avaliado</string>\n    <string name=\"sort_average_rating_desc\">Melhor avaliado</string>\n    <string name=\"sort_release_date_asc\">Mais velho</string>\n    <string name=\"sort_release_date_desc\">Mais recente</string>\n    <string name=\"title_media_type\">Tipo de média</string>\n    <string name=\"title_character_appearances\">Aparições da personagem</string>\n    <string name=\"title_categories\">Categorias</string>\n    <string name=\"title_filter\">Filtro</string>\n    <string name=\"title_description\">Descrição</string>\n    <string name=\"title_details\">Pormenores</string>\n    <string name=\"title_streamer\">Ligações de streaming</string>\n    <string name=\"title_ratings\">Avaliações</string>\n    <string name=\"title_more_franchise\">Mais desta Série</string>\n    <string name=\"title_favorite_anime\">Animes Favoritos</string>\n    <string name=\"title_favorite_manga\">Mangás Favoritos</string>\n    <string name=\"title_favorite_characters\">Personagens Favoritos</string>\n    <string name=\"title_authentication\">Autenticação</string>\n    <string name=\"title_login\">Logue com a sua conta do Kitsu</string>\n    <string name=\"title_play_trailer\">Reproduzir trailer</string>\n    <string name=\"title_about_me\">Sobre mim</string>\n    <string name=\"title_episodes\">Episódios</string>\n    <string name=\"title_chapters\">Capítulos</string>\n    <string name=\"title_characters\">Personagens</string>\n    <string name=\"title_edit_library_entry\">Editar entrada da biblioteca</string>\n    <string name=\"title_edit_profile\">Editar perfil</string>\n    <string name=\"no_information\">Desconhecido</string>\n    <string name=\"no_data_available\">Dados indisponíveis</string>\n    <string name=\"status_current\">Atualmente em exibição</string>\n    <string name=\"status_current_manga\">Em publicação</string>\n    <string name=\"status_finished\">Finalizado</string>\n    <string name=\"status_tba\">TBA</string>\n    <string name=\"status_unreleased\">Não lançado</string>\n    <string name=\"status_upcoming\">Brevemente</string>\n    <string name=\"character_role_main\">Principal</string>\n    <string name=\"character_role_supporting\">Apoio</string>\n    <string name=\"character_role_recurring\">Recorrente</string>\n    <string name=\"character_role_cameo\">Aparição</string>\n    <string name=\"library_status_watching\">Assistindo</string>\n    <string name=\"library_status_planned\">Planejo Assistir</string>\n    <string name=\"library_status_completed\">Completo</string>\n    <string name=\"library_status_on_hold\">Em Espera</string>\n    <string name=\"library_status_dropped\">Dropado</string>\n    <string name=\"library_status_reading\">Lendo</string>\n    <string name=\"library_status_planned_manga\">Planejo de ler</string>\n    <string name=\"library_action_remove\">Remover da biblioteca</string>\n    <string name=\"library_action_add\">Adicionar a biblioteca</string>\n    <string name=\"library_action_edit\">Editar entrada da biblioteca</string>\n    <string name=\"data_rank\">Posição #%d</string>\n    <string name=\"data_popularity\">Popularidade #%d</string>\n    <string name=\"data_kitsu_score\">Avaliação Kitsu %s%%</string>\n    <string name=\"data_length_total\">%s Total</string>\n    <string name=\"data_length_each\">%d minutos cada</string>\n    <string name=\"data_title_english\">Inglês</string>\n    <string name=\"data_title_japanese\">Japonês</string>\n    <string name=\"data_title_japanese_romaji\">Japonês (Romaji)</string>\n    <string name=\"data_abbreviated_titles\">Sinónimos</string>\n    <string name=\"data_other_names\">Também conhecido como</string>\n    <string name=\"data_title_type\">Tipo</string>\n    <string name=\"data_title_status\">Estado</string>\n    <string name=\"data_title_aired\">Exibição</string>\n    <string name=\"data_title_published\">Publicado</string>\n    <string name=\"data_title_tba\">Esperado</string>\n    <string name=\"data_title_season\">Temporada</string>\n    <string name=\"data_title_age_rating\">Avaliação</string>\n    <string name=\"data_title_serialization\">Serialização</string>\n    <string name=\"data_title_chapters\">Capítulos</string>\n    <string name=\"data_title_episodes\">Episódios</string>\n    <string name=\"data_title_length\">Duração</string>\n    <string name=\"data_title_producers\">Produtores</string>\n    <string name=\"data_title_licensors\">Licenciadores</string>\n    <string name=\"data_title_studios\">Estúdios</string>\n    <string name=\"season_winter\">Inverno</string>\n    <string name=\"season_spring\">Primavera</string>\n    <string name=\"season_summer\">Verão</string>\n    <string name=\"season_fall\">Outono</string>\n    <string name=\"relationship_sequel\">Sequência</string>\n    <string name=\"relationship_prequel\">Anterior</string>\n    <string name=\"relationship_alternative_setting\">Configuração alternativa</string>\n    <string name=\"relationship_alternative_version\">Versão alternativa</string>\n    <string name=\"relationship_side_story\">História secundária</string>\n    <string name=\"relationship_parent_story\">História Parente</string>\n    <string name=\"relationship_summary\">Resumo</string>\n    <string name=\"relationship_full_story\">História Completa</string>\n    <string name=\"relationship_adaptation\">Adaptação</string>\n    <string name=\"relationship_character\">Personagem</string>\n    <string name=\"relationship_other\">Outro</string>\n    <string name=\"section_trending\">Tendência da Semana</string>\n    <string name=\"section_top_airing_anime\">Top Animes em Exibição</string>\n    <string name=\"section_top_upcoming_anime\">Top Animes Futuros</string>\n    <string name=\"section_highest_rated_anime\">Animes Mais Bem Avaliados</string>\n    <string name=\"section_most_popular_anime\">Animes Mais Populares</string>\n    <string name=\"section_top_airing_manga\">Top Mangás em Publicação</string>\n    <string name=\"section_top_upcoming_manga\">Top Mangás Futuros</string>\n    <string name=\"section_highest_rated_manga\">Mangás Mais Bem Avaliados</string>\n    <string name=\"section_most_popular_manga\">Mangás Mais Populares</string>\n    <string name=\"preference_category_ui\">Interface do Utilizador</string>\n    <string name=\"preference_category_account\">Conta</string>\n    <string name=\"preference_category_advanced\">Avançado</string>\n    <string name=\"preference_category_about\">Sobre</string>\n    <string name=\"preference_appearance_description\">Mude o tema e ative o modo escuro.</string>\n    <string name=\"preference_dark_mode\">Modo Escuro</string>\n    <string name=\"preference_dark_mode_light\">Claro</string>\n    <string name=\"preference_dark_mode_dark\">Escuro</string>\n    <string name=\"preference_dark_mode_follow_system\">Seguir o Sistema</string>\n    <string name=\"preference_dark_mode_battery_saver\">Economia de Pilha</string>\n    <string name=\"preference_oled_black_mode\">Preto OLED</string>\n    <string name=\"preference_oled_black_mode_description\">Use o fundo preto no modo escuro.</string>\n    <string name=\"preference_app_theme\">Tema</string>\n    <string name=\"preference_dynamic_color_theme\">Usar Cor Dinâmica</string>\n    <string name=\"preference_dynamic_color_theme_description\">Adapta ao tema do sistema.</string>\n    <string name=\"preference_app_theme_default\">Padrão</string>\n    <string name=\"preference_app_theme_purple\">Roxo</string>\n    <string name=\"preference_app_theme_blue\">Azul</string>\n    <string name=\"preference_app_theme_green\">Verde</string>\n    <string name=\"preference_media_item_size\">Tamanho da Imagem do Pôster</string>\n    <string name=\"preference_media_item_size_small\">Pequeno</string>\n    <string name=\"preference_media_item_size_medium\">Médio</string>\n    <string name=\"preference_media_item_size_large\">Grande</string>\n    <string name=\"preference_start_fragment\">Página Padrão</string>\n    <string name=\"preference_start_fragment_home\">Início</string>\n    <string name=\"preference_start_fragment_search\">Pesquisar</string>\n    <string name=\"preference_start_fragment_library\">Biblioteca</string>\n    <string name=\"preference_start_fragment_profile\">Perfil</string>\n    <string name=\"preference_start_fragment_description\">A página padrão está selecionada para %s.</string>\n    <string name=\"preference_language\">Idioma</string>\n    <string name=\"preference_language_default\">Idioma padrão</string>\n    <string name=\"preference_display_name\">Nome de Exibição</string>\n    <string name=\"preference_profile_url\">URL do Perfil</string>\n    <string name=\"preference_profile_url_not_set\">Nenhuma URL de perfil definida.</string>\n    <string name=\"preference_country\">País</string>\n    <string name=\"preference_country_summary\">País está definido para %s</string>\n    <string name=\"preference_country_summary_non\">Sem país definido.</string>\n    <string name=\"preference_titles\">Títulos</string>\n    <string name=\"preference_titles_description\">Defina como os títulos das médias serão mostrados.</string>\n    <string name=\"preference_titles_canonical\">Canónico</string>\n    <string name=\"preference_titles_romanized\">Romanizado</string>\n    <string name=\"preference_titles_english\">Inglês</string>\n    <string name=\"preference_adult_content\">Conteúdo adulto</string>\n    <string name=\"preference_adult_content_description_sfw\">Médias classificadas como 18+ serão ocultadas.</string>\n    <string name=\"preference_adult_content_description_sometimes\">Médias classificadas como 18+ serão visíveis, mas as postagens marcadas como NSFW serão ocultadas no feed global.</string>\n    <string name=\"preference_adult_content_description_everywhere\">Médias classificadas como +18 serão visíveis em todo lugar.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Ocultar todo conteúdo adulto</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Limitar ao feed seguidos</string>\n    <string name=\"preference_sfw_filter_show_all\">Conteúdo adulto em todo lugar</string>\n    <string name=\"preference_rating_system\">Sistema de avaliação</string>\n    <string name=\"preference_rating_system_simple\">Simples</string>\n    <string name=\"preference_rating_system_advanced\">Avançado</string>\n    <string name=\"preference_remember_search_filters\">Lembrar filtros de pesquisa</string>\n    <string name=\"preference_remember_search_filters_description\">Aplicar o último filtro de pesquisa usado após fechar a app.</string>\n    <string name=\"preference_force_legacy_image_picker\">Forçar seletor de Fotos Herdado</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Usar o seletor de fotos do sistema em vez do novo seletor de fotos.</string>\n    <string name=\"preference_check_for_updates\">Verificar por atualizações na antrada</string>\n    <string name=\"preference_check_for_updates_description\">Verificar por uma nova versão da app sempre que abrir a app.</string>\n    <string name=\"preference_app_logs_description\">Ver e partilhar mensagens de registo da aplicação.</string>\n    <string name=\"preference_app_version\">Informações da versão</string>\n    <string name=\"preference_app_version_description\">Toque para verificar por atualizações.</string>\n    <string name=\"preference_github_repo\">Repositório GitHub</string>\n    <string name=\"preference_open_source_libraries_description\">A lista de todas as bibliotecas de código aberto usadas.</string>\n    <string name=\"preference_not_logged_in\">Precisa estar logado para editar esta configuração.</string>\n    <string name=\"app_logs_no_data\">Nenhum registo disponível.</string>\n    <string name=\"prompt_email\">Email</string>\n    <string name=\"prompt_password\">Palavra-passe</string>\n    <string name=\"action_sign_in\">Registar</string>\n    <string name=\"action_log_in\">Logar</string>\n    <string name=\"action_log_in_to_kitsu\">Logar no Kitsu</string>\n    <string name=\"action_create_account\">Criar conta</string>\n    <string name=\"invalid_username\">Não é um endereço de email válido</string>\n    <string name=\"login_failed\">Falha ao logar</string>\n    <string name=\"status_logging_in\">A logar…</string>\n    <string name=\"logged_in_success\">Logado como %s.</string>\n    <string name=\"login_no_account\">Não tem uma conta? Crie uma em <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"not_logged_in\">Não logado</string>\n    <string name=\"library_not_started\">Não Iniciado</string>\n    <string name=\"library_not_rated\">Não Avaliado</string>\n    <string name=\"library_not_logged_in_title\">Logue para aceder a sua biblioteca</string>\n    <string name=\"library_not_logged_in_text\">Logue com a sua conta do Kitsu para gerir a sua biblioteca e ter acesso a todos os recursos.</string>\n    <string name=\"library_kind_all\">Tudo</string>\n    <string name=\"library_not_synchronized\">Não sincronizado</string>\n    <string name=\"library_edit_status\">Estado da biblioteca</string>\n    <string name=\"library_edit_progress\">Progresso</string>\n    <string name=\"library_edit_rating\">Avaliação</string>\n    <string name=\"library_edit_reread_count\">Contagem de relido</string>\n    <string name=\"library_edit_start_reread\">Começar a reler</string>\n    <string name=\"library_edit_privacy\">Privacidade</string>\n    <string name=\"library_edit_privacy_public\">Público</string>\n    <string name=\"library_edit_privacy_private\">Privado</string>\n    <string name=\"library_edit_started\">Começou</string>\n    <string name=\"library_edit_finished\">Finalizou</string>\n    <string name=\"library_edit_no_date_set\">Sem data definida</string>\n    <string name=\"library_edit_notes\">Notas pessoais</string>\n    <string name=\"info_log_in_required\">Logue para ter acesso a todos os recursos.</string>\n    <string name=\"info_image_saved_in_gallery\">Imagem gravada com sucesso na galeria.</string>\n    <string name=\"info_update_checking_new_version\">A verificar por uma nova versão…</string>\n    <string name=\"info_update_new_version_available\">Nova versão disponível.</string>\n    <string name=\"info_update_new_version_available_text\">Versão %s do Kitsune está disponível.</string>\n    <string name=\"info_update_no_new_version_available\">Nenhuma nova versão está disponível.</string>\n    <string name=\"info_update_failed\">Falha ao verificar por uma nova versão.</string>\n    <string name=\"info_logged_out_token_expired\">O seu token de acesso expirou. Por favor logue novamente.</string>\n    <string name=\"filter_kind\">Categoria</string>\n    <string name=\"filter_year\">Ano</string>\n    <string name=\"filter_avg_rating\">Avaliação média</string>\n    <string name=\"filter_select_categories\">Selecionar categorias</string>\n    <string name=\"filter_season\">Temporada</string>\n    <string name=\"filter_subtype\">Subtipo</string>\n    <string name=\"filter_streamers\">Serviços</string>\n    <string name=\"filter_age_rating\">Faixa etária</string>\n    <string name=\"profile_not_logged_in_title\">Nada a ver aqui!</string>\n    <string name=\"profile_not_logged_in_text\">Logue na sua conta do Kitsu ou crie uma nova conta para ter acesso a todos os recursos.</string>\n    <string name=\"profile_anime_stats\">Estatísticas de anime</string>\n    <string name=\"profile_manga_stats\">Estatísticas de Mangá</string>\n    <string name=\"profile_stats_anime_watch_time\">%s gastas assistindo anime.</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d capítulos lidos.</string>\n    <string name=\"profile_stats_time_spent_total\">%s no total.</string>\n    <string name=\"profile_stats_completed\">%1$d completos. Mais do que <b>%2$d%%</b> dos utilizadores.</string>\n    <string name=\"profile_data_gender\">Género</string>\n    <string name=\"profile_data_location\">Localização</string>\n    <string name=\"profile_data_birthday\">Aniversário</string>\n    <string name=\"profile_data_join_date\">Data de entrada</string>\n    <string name=\"profile_data_join_date_ago\">%s atrás</string>\n    <string name=\"profile_data_private\">É um segredo.</string>\n    <string name=\"profile_gender_male\">Masculino</string>\n    <string name=\"profile_gender_female\">Feminino</string>\n    <string name=\"profile_gender_custom\">Customizado</string>\n    <string name=\"profile_gender_custom_hint\">Defina o seu gênero</string>\n    <string name=\"profile_waifu_or_husbando\">Waifu ou Husbando</string>\n    <string name=\"profile_find_waifu\">Encontre o seu alguém especial</string>\n    <string name=\"profile_cover_image_description\">Imagem da capa</string>\n    <string name=\"profile_avatar_image_description\">imagem do perfil</string>\n    <string name=\"profile_link_url\">URL ou Nome de utilizador</string>\n    <string name=\"notification_updates\">Atualizações da aplicação</string>\n    <string name=\"widget_description\">Progresso da biblioteca</string>\n    <string name=\"widget_empty_text\">Não há nenhum elemento rastreado ativo na sua biblioteca</string>\n    <string name=\"widget_empty_action\">Abrir biblioteca</string>\n    <string name=\"onboarding_welcome_title\">Bem-vindo ao Kitsune!</string>\n    <string name=\"onboarding_welcome_subtitle\">O seu companheiro pessoal de animes e mangas.</string>\n    <string name=\"onboarding_welcome_feature_search\">Pesquise</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Encontre os seus animes e mangas favoritos.</string>\n    <string name=\"onboarding_welcome_feature_explore\">Explore</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Descubra títulos novos e em alta.</string>\n    <string name=\"onboarding_welcome_action\">Comece agora</string>\n    <string name=\"onboarding_login_title\">Faça login no Kitsu</string>\n    <string name=\"onboarding_login_subtitle\">Faça login na sua conta Kitsu ou crie uma nova para ter acesso a todas as funcionalidades.</string>\n    <string name=\"onboarding_login_is_logged_in\">Está conectado</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Criar conta</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Será redirecionado para %1$s, onde poderá criar uma nova conta.</string>\n    <string name=\"onboarding_setup_title\">Configuração Inicial</string>\n    <string name=\"onboarding_setup_subtitle\">Configure as suas preferências para começar. Poderá mudá-las a qualquer momento nas configurações.</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">Permissão de notificação necessária.</string>\n    <string name=\"onboarding_setup_updates\">Verificar por atualizações</string>\n    <string name=\"onboarding_setup_updates_description\">Seja notificado quando uma nova versão estiver disponível no GitHub.</string>\n    <string name=\"onboarding_setup_title_language\">Idioma de Títulos</string>\n    <string name=\"onboarding_setup_title_language_description\">Selecione o seu idioma preferido para títulos.</string>\n    <string name=\"onboarding_setup_title_language_selected\">Selecionado: %1$s</string>\n    <string name=\"onboarding_setup_action\">Concluir Configuração</string>\n    <string name=\"unit_episode\">Episódio %d</string>\n    <string name=\"unit_chapter\">Capítulo %d</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s Página</item>\n        <item quantity=\"many\">%s Páginas</item>\n        <item quantity=\"other\">%s Páginas</item>\n    </plurals>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s segundo</item>\n        <item quantity=\"many\">%s segundos</item>\n        <item quantity=\"other\">%s segundos</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s minuto</item>\n        <item quantity=\"many\">%s minutos</item>\n        <item quantity=\"other\">%s minutos</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s hora</item>\n        <item quantity=\"many\">%s horas</item>\n        <item quantity=\"other\">%s horas</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s dia</item>\n        <item quantity=\"many\">%s dias</item>\n        <item quantity=\"other\">%s dias</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s mês</item>\n        <item quantity=\"many\">%s meses</item>\n        <item quantity=\"other\">%s meses</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s ano</item>\n        <item quantity=\"many\">%s anos</item>\n        <item quantity=\"other\">%s anos</item>\n    </plurals>\n    <string name=\"data_title_volumes\">Volumes</string>\n    <string name=\"preference_rating_system_regular\">Regular</string>\n    <string name=\"library_edit_volumes\">Volumes</string>\n    <string name=\"profile_data_waifu\">Waifu</string>\n    <string name=\"profile_data_husbando\">Husbando</string>\n    <string name=\"profile_data_bio\">Bio</string>\n    <string name=\"onboarding_welcome_feature_track\">Seguir</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Siga o controlo do que viu e leu.</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-pt-rBR/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"nav_profile\">Perfil</string>\n    <string name=\"nav_app_logs\">Registros do aplicativo</string>\n    <string name=\"nav_os_libraries\">Bibliotecas de código aberto</string>\n    <string name=\"nav_search\">Pesquisar</string>\n    <string name=\"nav_library\">Biblioteca</string>\n    <string name=\"nav_settings\">Configurações</string>\n    <string name=\"nav_appearance\">Aparência</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"manga\">Mangá</string>\n    <string name=\"nav_home\">Início</string>\n    <string name=\"action_retry\">Tentar novamente</string>\n    <string name=\"action_ok\">OK</string>\n    <string name=\"action_cancel\">Cancelar</string>\n    <string name=\"action_reset_filter\">Redefinir filtro</string>\n    <string name=\"action_unselect_all\">Desmarque todos</string>\n    <string name=\"action_read_more\">Ler mais</string>\n    <string name=\"action_read_less\">Ler menos</string>\n    <string name=\"action_show_more\">Mostrar mais</string>\n    <string name=\"action_show_less\">Mostrar menos</string>\n    <string name=\"action_view\">Visualizar</string>\n    <string name=\"action_back\">Voltar</string>\n    <string name=\"action_close\">Fechar</string>\n    <string name=\"action_log_out\">Deslogar</string>\n    <string name=\"action_share_profile_url\">Compartilhar Perfil</string>\n    <string name=\"action_share\">Compartilhar</string>\n    <string name=\"action_open_external\">Abrir em um site externo</string>\n    <string name=\"action_open_external_short\">Sites externos</string>\n    <string name=\"action_share_app_logs\">Compartilhar registros</string>\n    <string name=\"action_add_to_favorites\">Adicionar aos favoritos</string>\n    <string name=\"action_start\">Começar</string>\n    <string name=\"action_allow\">Permitir</string>\n    <string name=\"action_rate\">Avaliar</string>\n    <string name=\"action_update_rating\">Atualizar avaliação</string>\n    <string name=\"action_remove_rating\">Remover avaliação</string>\n    <string name=\"action_add_profile_link\">Adicionar link ao perfil</string>\n    <string name=\"action_dismiss\">Descartar</string>\n    <string name=\"action_synchronize\">Sincronizar</string>\n    <string name=\"action_open_on_mal\">Abrir no MyAnimeList.net</string>\n    <string name=\"action_save_changes\">Salvar Mudanças</string>\n    <string name=\"action_edit_profile\">Editar Perfil</string>\n    <string name=\"action_add\">Adicionar</string>\n    <string name=\"action_edit_profile_link\">Editar Link do Perfil</string>\n    <string name=\"action_select_profile_link_site\">Selecionar um Site</string>\n    <string name=\"hint_search\">Pesquisar</string>\n    <string name=\"hint_search_characters\">Pesquisar personagens</string>\n    <string name=\"hint_mark_watched\">Marcar como assistido.</string>\n    <string name=\"hint_mark_not_watched\">Marcar como não assistido.</string>\n    <string name=\"hint_rating\">Avaliar</string>\n    <string name=\"dialog_remove_from_library_title\">Remover da biblioteca?</string>\n    <string name=\"dialog_remove_from_library_msg\"><b>%s</b> vai ser removido da sua biblioteca.</string>\n    <string name=\"dialog_log_out_confirmation\">Você tem certeza que quer deslogar?</string>\n    <string name=\"error_nothing_found\">Não encontrado.</string>\n    <string name=\"error_invalid_user\">Usuário Inválido. Você está logado?</string>\n    <string name=\"error_user_update_failed\">Falha ao atualizar os dados do usuário.</string>\n    <string name=\"error_mapping_loading\">Falha ao carregar mapeamento de sites externos.</string>\n    <string name=\"error_user_delete_waifu_failed\">Falha ao remover Waifu/Husbando.</string>\n    <string name=\"error_user_update_image_failed\">Falha ao atualizar a imagem de perfil ou capa.</string>\n    <string name=\"error_something_wrong\">Oops, algo deu errado!</string>\n    <string name=\"error_requires_external_storage_permission\">Requer permissão para salvar no armazenamento externo.</string>\n    <string name=\"error_library_update_failed\">Falha ao atualizar a entrada na biblioteca.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Falha ao remover o link do perfil do %s.</string>\n    <string name=\"error_library_update_failed_multiple\">Falha ao atualizar %d entradas na biblioteca.</string>\n    <string name=\"error_requires_notification_permission\">Permissão as notificações é necessária.</string>\n    <string name=\"sort_popularity_asc\">Menos Popular</string>\n    <string name=\"sort_popularity_desc\">Mais Popular</string>\n    <string name=\"sort_average_rating_asc\">Pior Avaliado</string>\n    <string name=\"sort_release_date_asc\">Mais Velho</string>\n    <string name=\"sort_release_date_desc\">Mais Recente</string>\n    <string name=\"title_media_type\">Tipo de Mídia</string>\n    <string name=\"title_character_appearances\">Aparições do Personagem</string>\n    <string name=\"title_categories\">Categorias</string>\n    <string name=\"title_description\">Descrição</string>\n    <string name=\"title_streamer\">Links de Streaming</string>\n    <string name=\"title_ratings\">Avaliações</string>\n    <string name=\"title_more_franchise\">Mais desta Série</string>\n    <string name=\"title_favorite_anime\">Animes Favoritos</string>\n    <string name=\"title_favorite_manga\">Mangás Favoritos</string>\n    <string name=\"error_library_delete_failed\">Falha ao remover entrada da biblioteca.</string>\n    <string name=\"title_authentication\">Autenticação</string>\n    <string name=\"title_play_trailer\">Reproduzir Trailer</string>\n    <string name=\"title_about_me\">Sobre Mim</string>\n    <string name=\"title_episodes\">Episódios</string>\n    <string name=\"title_chapters\">Capítulos</string>\n    <string name=\"title_edit_library_entry\">Editar Entrada da Biblioteca</string>\n    <string name=\"title_edit_profile\">Editar Perfil</string>\n    <string name=\"no_information\">Desconhecido</string>\n    <string name=\"no_data_available\">Dados indisponíveis</string>\n    <string name=\"status_current\">Atualmente em Exibição</string>\n    <string name=\"status_current_manga\">Em Publicação</string>\n    <string name=\"status_unreleased\">Não Lançado</string>\n    <string name=\"status_upcoming\">Em Breve</string>\n    <string name=\"character_role_main\">Principal</string>\n    <string name=\"character_role_supporting\">Suporte</string>\n    <string name=\"character_role_cameo\">Aparição</string>\n    <string name=\"status_tba\">TBA</string>\n    <string name=\"library_status_planned\">Planejo Assistir</string>\n    <string name=\"library_status_completed\">Completo</string>\n    <string name=\"library_status_on_hold\">Em Espera</string>\n    <string name=\"library_status_planned_manga\">Planejo Ler</string>\n    <string name=\"library_action_remove\">Remover da Biblioteca</string>\n    <string name=\"library_action_edit\">Editar Entrada da Biblioteca</string>\n    <string name=\"data_rank\">Posição #%d</string>\n    <string name=\"data_kitsu_score\">Avaliação Kitsu %s%%</string>\n    <string name=\"data_length_total\">%s Total</string>\n    <string name=\"data_length_each\">%d minutos cada</string>\n    <string name=\"data_title_english\">Inglês</string>\n    <string name=\"data_title_status\">Estado</string>\n    <string name=\"data_title_aired\">Exibição</string>\n    <string name=\"data_title_published\">Publicado</string>\n    <string name=\"data_title_tba\">Esperado</string>\n    <string name=\"data_title_season\">Temporada</string>\n    <string name=\"data_title_age_rating\">Avaliação</string>\n    <string name=\"data_title_chapters\">Capítulos</string>\n    <string name=\"season_spring\">Primavera</string>\n    <string name=\"season_summer\">Verão</string>\n    <string name=\"season_fall\">Outono</string>\n    <string name=\"relationship_sequel\">Sequência</string>\n    <string name=\"relationship_prequel\">Anterior</string>\n    <string name=\"relationship_alternative_setting\">Configuração alternativa</string>\n    <string name=\"relationship_alternative_version\">Versão alternativa</string>\n    <string name=\"relationship_spinoff\">Spin-off</string>\n    <string name=\"relationship_adaptation\">Adaptação</string>\n    <string name=\"relationship_character\">Personagem</string>\n    <string name=\"relationship_other\">Outro</string>\n    <string name=\"section_trending\">Tendência da Semana</string>\n    <string name=\"section_highest_rated_anime\">Animes Mais Bem Avaliados</string>\n    <string name=\"section_top_upcoming_manga\">Top Mangás Futuros</string>\n    <string name=\"section_highest_rated_manga\">Mangás Mais Bem Avaliados</string>\n    <string name=\"section_most_popular_manga\">Mangás Mais Populares</string>\n    <string name=\"preference_category_ui\">Interface do Usuário</string>\n    <string name=\"preference_category_account\">Conta</string>\n    <string name=\"preference_oled_black_mode_description\">Use o fundo preto no modo escuro.</string>\n    <string name=\"preference_app_theme\">Tema</string>\n    <string name=\"preference_app_theme_default\">Padrão</string>\n    <string name=\"preference_app_theme_purple\">Roxo</string>\n    <string name=\"preference_dynamic_color_theme\">Usar Cor Dinâmica</string>\n    <string name=\"preference_media_item_size\">Tamanho da Imagem do Pôster</string>\n    <string name=\"preference_media_item_size_large\">Grande</string>\n    <string name=\"preference_start_fragment\">Página Padrão</string>\n    <string name=\"preference_start_fragment_home\">Início</string>\n    <string name=\"preference_start_fragment_search\">Pesquisar</string>\n    <string name=\"preference_start_fragment_library\">Biblioteca</string>\n    <string name=\"preference_start_fragment_profile\">Perfil</string>\n    <string name=\"preference_start_fragment_description\">A página padrão está selecionada pra %s.</string>\n    <string name=\"preference_titles_description\">Defina como os títulos das mídias serão mostrados.</string>\n    <string name=\"preference_titles_canonical\">Canônico</string>\n    <string name=\"preference_titles_romanized\">Romanizado</string>\n    <string name=\"preference_adult_content_description_sfw\">Mídias classificadas como 18+ serão ocultadas.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Ocultar todo Conteúdo Adulto</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Limitar ao Feed Seguidos</string>\n    <string name=\"preference_adult_content_description_sometimes\">Mídias classificadas como 18+ serão visíveis, mas as postagens marcadas como NSFW serão ocultadas no feed global.</string>\n    <string name=\"preference_adult_content_description_everywhere\">Mídias classificadas como +18 serão visíveis em todo lugar.</string>\n    <string name=\"preference_force_legacy_image_picker\">Forçar seletor de Fotos Herdado</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Usar o seletor de fotos do sistema em vez do novo seletor de fotos.</string>\n    <string name=\"preference_check_for_updates\">Checar por Atualizações na Entrada</string>\n    <string name=\"preference_app_version\">Informações da Versão</string>\n    <string name=\"preference_app_version_description\">Toque para checar por atualizações.</string>\n    <string name=\"preference_github_repo\">Repositório GitHub</string>\n    <string name=\"preference_check_for_updates_description\">Chegar por uma nova versão do app sempre que abrir o app.</string>\n    <string name=\"preference_open_source_libraries_description\">A lista de todas as bibliotecas de código aberto usadas.</string>\n    <string name=\"preference_not_logged_in\">Você precisa estar logado para editar essa configuração.</string>\n    <string name=\"status_logging_in\">Logando…</string>\n    <string name=\"logged_in_success\">Logado como %s.</string>\n    <string name=\"login_no_account\">Não tem uma conta? Crie uma em <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"not_logged_in\">Não logado</string>\n    <string name=\"library_not_started\">Não Iniciado</string>\n    <string name=\"library_not_logged_in_title\">Logue para acessar sua biblioteca</string>\n    <string name=\"library_kind_all\">Tudo</string>\n    <string name=\"library_not_synchronized\">Não Sincronizado</string>\n    <string name=\"library_edit_status\">Estado da Biblioteca</string>\n    <string name=\"library_edit_progress\">Progresso</string>\n    <string name=\"library_edit_rewatch_count\">Contagem de Reassistido</string>\n    <string name=\"library_edit_reread_count\">Contagem de Relido</string>\n    <string name=\"library_edit_start_rewatch\">Começar a Reassistir</string>\n    <string name=\"library_edit_start_reread\">Começar a Reler</string>\n    <string name=\"library_edit_privacy\">Privacidade</string>\n    <string name=\"library_edit_privacy_public\">Público</string>\n    <string name=\"library_edit_privacy_private\">Privado</string>\n    <string name=\"library_edit_started\">Começou</string>\n    <string name=\"library_edit_no_date_set\">Sem Data definida</string>\n    <string name=\"info_log_in_required\">Logue para ter acesso a todos os recursos.</string>\n    <string name=\"info_image_saved_in_gallery\">Imagem salva com sucesso na galeria.</string>\n    <string name=\"info_update_new_version_available_text\">Versão %s do Kitsune está disponível.</string>\n    <string name=\"info_update_no_new_version_available\">Nenhuma nova versão está disponível.</string>\n    <string name=\"filter_kind\">Categoria</string>\n    <string name=\"filter_subtype\">Subtipo</string>\n    <string name=\"profile_not_logged_in_title\">Nada para ver aqui!</string>\n    <string name=\"profile_not_logged_in_text\">Logue na sua conta do Kitsu ou crie uma nova conta para ter acesso a todos os recursos.</string>\n    <string name=\"profile_stats_anime_watch_time\">%s gastas assistindo anime.</string>\n    <string name=\"profile_manga_stats\">Estatísticas de Mangá</string>\n    <string name=\"profile_stats_time_spent_total\">%s no total.</string>\n    <string name=\"profile_stats_completed\">%1$d completos. Mais do que <b>%2$d%%</b> dos usuários.</string>\n    <string name=\"profile_data_gender\">Gênero</string>\n    <string name=\"profile_data_location\">Localização</string>\n    <string name=\"profile_gender_female\">Feminino</string>\n    <string name=\"profile_gender_custom\">Customizado</string>\n    <string name=\"profile_gender_custom_hint\">Defina o seu gênero</string>\n    <string name=\"profile_data_husbando\">Husbando</string>\n    <string name=\"profile_waifu_or_husbando\">Waifu ou Husbando</string>\n    <string name=\"profile_find_waifu\">Encontre seu alguém especial</string>\n    <string name=\"profile_data_bio\">Bio</string>\n    <string name=\"profile_cover_image_description\">Imagem da capa</string>\n    <string name=\"profile_avatar_image_description\">imagem do Perfil</string>\n    <string name=\"profile_link_url\">URL ou Nome de Usuário</string>\n    <string name=\"notification_updates\">Atualizações do Aplicativo</string>\n    <string name=\"unit_episode\">Episódio %d</string>\n    <string name=\"unit_chapter\">Capítulo %d</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s Página</item>\n        <item quantity=\"many\">%s Páginas</item>\n        <item quantity=\"other\">%s Páginas</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s hora</item>\n        <item quantity=\"many\">%s horas</item>\n        <item quantity=\"other\">%s horas</item>\n    </plurals>\n    <string name=\"action_delete_profile_link\">Remover Link do Perfil</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Permissão rejeitada</string>\n    <string name=\"dialog_notification_permission_rejected\">Você não vai receber notificações e nem será informado sobre atualizações do aplicativo.</string>\n    <string name=\"dialog_request_notification_permission_title\">Permitir notificações</string>\n    <string name=\"dialog_request_notification_permission\">Permitir ser notificado quando uma nova atualização estiver disponível.</string>\n    <string name=\"error_resource_loading\">Não foi possível carregar os dados.</string>\n    <string name=\"error_image_loading\">Falha ao carregar a imagens.</string>\n    <string name=\"error_search_provider_unavailable\">Provedor de pesquisa indisponível.</string>\n    <string name=\"error_library_update_not_found\">A entrada na biblioteca não existe.</string>\n    <string name=\"error_library_add_failed\">Falha ao adicionar à sua biblioteca.</string>\n    <string name=\"sort_average_rating_desc\">Melhor Avaliado</string>\n    <string name=\"title_filter\">Filtro</string>\n    <string name=\"title_details\">Detalhes</string>\n    <string name=\"title_favorite_characters\">Personagens Favoritos</string>\n    <string name=\"title_characters\">Personagens</string>\n    <string name=\"status_finished\">Finalizado</string>\n    <string name=\"character_role_recurring\">Recorrente</string>\n    <string name=\"library_status_watching\">Assistindo</string>\n    <string name=\"library_status_dropped\">Dropado</string>\n    <string name=\"library_status_reading\">Lendo</string>\n    <string name=\"library_action_add\">Adicionar a Biblioteca</string>\n    <string name=\"data_popularity\">Popularidade #%d</string>\n    <string name=\"data_title_japanese\">Japonês</string>\n    <string name=\"data_title_japanese_romaji\">Japonês (Romaji)</string>\n    <string name=\"data_abbreviated_titles\">Sinônimos</string>\n    <string name=\"data_other_names\">Também conhecido como</string>\n    <string name=\"data_title_type\">Tipo</string>\n    <string name=\"data_title_volumes\">Volumes</string>\n    <string name=\"data_title_serialization\">Serialização</string>\n    <string name=\"data_title_licensors\">Licenciadores</string>\n    <string name=\"data_title_episodes\">Episódios</string>\n    <string name=\"data_title_length\">Duração</string>\n    <string name=\"data_title_producers\">Produtores</string>\n    <string name=\"season_winter\">Inverno</string>\n    <string name=\"data_title_studios\">Estúdios</string>\n    <string name=\"relationship_side_story\">História secundária</string>\n    <string name=\"relationship_summary\">Resumo</string>\n    <string name=\"relationship_parent_story\">História Parente</string>\n    <string name=\"relationship_full_story\">História Completa</string>\n    <string name=\"section_top_airing_anime\">Top Animes em Exibição</string>\n    <string name=\"section_top_upcoming_anime\">Top Animes Futuros</string>\n    <string name=\"section_most_popular_anime\">Animes Mais Populares</string>\n    <string name=\"section_top_airing_manga\">Top Mangás em Publicação</string>\n    <string name=\"preference_category_advanced\">Avançado</string>\n    <string name=\"preference_category_about\">Sobre</string>\n    <string name=\"preference_appearance_description\">Mude o tema e ative o modo escuro.</string>\n    <string name=\"preference_dark_mode\">Modo Escuro</string>\n    <string name=\"preference_dark_mode_light\">Claro</string>\n    <string name=\"preference_dark_mode_dark\">Escuro</string>\n    <string name=\"preference_dark_mode_follow_system\">Seguir o Sistema</string>\n    <string name=\"preference_dark_mode_battery_saver\">Economia de Bateria</string>\n    <string name=\"preference_oled_black_mode\">Preto OLED</string>\n    <string name=\"preference_dynamic_color_theme_description\">Adapta ao tema do sistema.</string>\n    <string name=\"preference_app_theme_blue\">Azul</string>\n    <string name=\"preference_app_theme_green\">Verde</string>\n    <string name=\"preference_media_item_size_small\">Pequeno</string>\n    <string name=\"preference_media_item_size_medium\">Médio</string>\n    <string name=\"preference_language_default\">Idioma padrão</string>\n    <string name=\"preference_display_name\">Nome de Exibição</string>\n    <string name=\"preference_language\">Idioma</string>\n    <string name=\"preference_profile_url_not_set\">Nenhuma URL de perfil definida.</string>\n    <string name=\"preference_profile_url\">URL do Perfil</string>\n    <string name=\"preference_country\">País</string>\n    <string name=\"preference_country_summary\">País está definido para %s</string>\n    <string name=\"preference_country_summary_non\">Sem país definido.</string>\n    <string name=\"preference_titles\">Títulos</string>\n    <string name=\"preference_titles_english\">Inglês</string>\n    <string name=\"preference_adult_content\">Conteúdo Adulto</string>\n    <string name=\"preference_rating_system_simple\">Simples</string>\n    <string name=\"preference_rating_system_advanced\">Avançado</string>\n    <string name=\"preference_rating_system_regular\">Regular</string>\n    <string name=\"preference_sfw_filter_show_all\">Conteúdo Adulto em Todo Lugar</string>\n    <string name=\"preference_rating_system\">Sistema de Avaliação</string>\n    <string name=\"preference_remember_search_filters\">Lembrar Filtros de Pesquisa</string>\n    <string name=\"preference_remember_search_filters_description\">Aplicar o último filtro de pesquisa usado após fechar o app.</string>\n    <string name=\"preference_app_logs_description\">Ver e compartilhar mensagens de registro do aplicativo.</string>\n    <string name=\"app_logs_no_data\">Nenhum registro disponível.</string>\n    <string name=\"prompt_email\">Email</string>\n    <string name=\"prompt_password\">Senha</string>\n    <string name=\"invalid_username\">Não é um endereço de email válido</string>\n    <string name=\"login_failed\">Falha ao logar</string>\n    <string name=\"library_not_rated\">Não Avaliado</string>\n    <string name=\"action_sign_in\">Registrar</string>\n    <string name=\"action_log_in\">Logar</string>\n    <string name=\"action_log_in_to_kitsu\">Logar no Kitsu</string>\n    <string name=\"library_not_logged_in_text\">Logue com sua conta do Kitsu para gerenciar sua biblioteca e ter acesso a todos os recursos.</string>\n    <string name=\"library_edit_rating\">Avaliação</string>\n    <string name=\"library_edit_volumes\">Volumes</string>\n    <string name=\"filter_select_categories\">Selecionar Categorias</string>\n    <string name=\"library_edit_finished\">Finalizou</string>\n    <string name=\"library_edit_notes\">Notas Pessoais</string>\n    <string name=\"info_update_checking_new_version\">Checando por uma nova versão…</string>\n    <string name=\"info_update_new_version_available\">Nova versão disponível.</string>\n    <string name=\"filter_year\">Ano</string>\n    <string name=\"info_update_failed\">Falha ao chegar por uma nova versão.</string>\n    <string name=\"info_logged_out_token_expired\">Seu token de acesso expirou. Por favor logue novamente.</string>\n    <string name=\"filter_streamers\">Serviços</string>\n    <string name=\"filter_age_rating\">Faixa Etária</string>\n    <string name=\"filter_avg_rating\">Avaliação Média</string>\n    <string name=\"filter_season\">Temporada</string>\n    <string name=\"profile_anime_stats\">Estatísticas de anime</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d capítulos lidos.</string>\n    <string name=\"profile_data_birthday\">Aniversário</string>\n    <string name=\"profile_data_join_date\">Data de Entrada</string>\n    <string name=\"profile_data_join_date_ago\">%s atrás</string>\n    <string name=\"profile_data_private\">É segredo.</string>\n    <string name=\"profile_gender_male\">Masculino</string>\n    <string name=\"profile_data_waifu\">Waifu</string>\n    <string name=\"action_remove\">Remover</string>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s minuto</item>\n        <item quantity=\"many\">%s minutos</item>\n        <item quantity=\"other\">%s minutos</item>\n    </plurals>\n    <string name=\"action_remove_from_favorites\">Remover dos favoritos</string>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s segundo</item>\n        <item quantity=\"many\">%s segundos</item>\n        <item quantity=\"other\">%s segundos</item>\n    </plurals>\n    <string name=\"action_save_in_gallery\">Salvar na Galeria</string>\n    <string name=\"action_open_in_browser\">Abrir no Navegador</string>\n    <string name=\"action_update_profile\">Atualizar Perfil</string>\n    <string name=\"action_update\">Atualizar</string>\n    <string name=\"error_user_update_profile_link_failed\">Falha ao atualizar o link do perfil do %s.</string>\n    <string name=\"error_user_create_profile_link_failed\">Falha ao criar o link do perfil do %s.</string>\n    <string name=\"title_login\">Logue com a sua conta do Kitsu</string>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s ano</item>\n        <item quantity=\"many\">%s anos</item>\n        <item quantity=\"other\">%s anos</item>\n    </plurals>\n    <string name=\"widget_description\">Progresso da biblioteca</string>\n    <string name=\"widget_empty_text\">Não há nenhum item rastreado ativo em sua biblioteca</string>\n    <string name=\"widget_empty_action\">Abrir Biblioteca</string>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s dia</item>\n        <item quantity=\"many\">%s dias</item>\n        <item quantity=\"other\">%s dias</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s mês</item>\n        <item quantity=\"many\">%s meses</item>\n        <item quantity=\"other\">%s meses</item>\n    </plurals>\n    <string name=\"action_skip\">Pular</string>\n    <string name=\"action_next\">Próximo</string>\n    <string name=\"action_continue\">Continuar</string>\n    <string name=\"onboarding_welcome_title\">Bem-vindo ao Kitsune!</string>\n    <string name=\"onboarding_welcome_subtitle\">Seu companheiro pessoal de animes e mangas.</string>\n    <string name=\"onboarding_welcome_feature_search\">Pesquise</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Encontre seus animes e mangas favoritos.</string>\n    <string name=\"onboarding_welcome_feature_explore\">Explore</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Descubra títulos novos e em alta.</string>\n    <string name=\"onboarding_welcome_feature_track\">Acompanhe</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Acompanhe o que você assistiu e leu.</string>\n    <string name=\"onboarding_welcome_action\">Comece agora</string>\n    <string name=\"onboarding_login_title\">Faça login no Kitsu</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Você será redirecionado para %1$s, onde poderá criar uma nova conta.</string>\n    <string name=\"onboarding_setup_title\">Configuração Inicial</string>\n    <string name=\"onboarding_setup_subtitle\">Configure suas preferências para começar. Você poderá mudá-las a qualquer momento nas configurações.</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">Permissão de notificação necessária.</string>\n    <string name=\"onboarding_setup_updates\">Verificar Atualizações</string>\n    <string name=\"onboarding_setup_title_language_description\">Selecione seu idioma preferido para títulos.</string>\n    <string name=\"onboarding_setup_title_language_selected\">Selecionado: %1$s</string>\n    <string name=\"onboarding_setup_action\">Concluir Configuração</string>\n    <string name=\"onboarding_login_subtitle\">Faça login na sua conta Kitsu ou crie uma nova para ter acesso a todas as funcionalidades.</string>\n    <string name=\"onboarding_login_is_logged_in\">Você está conectado</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Criar Conta</string>\n    <string name=\"action_create_account\">Criar conta</string>\n    <string name=\"onboarding_setup_updates_description\">Seja notificado quando uma nova versão estiver disponível no GitHub.</string>\n    <string name=\"onboarding_setup_title_language\">Idioma de Títulos</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"nav_home\">Главная</string>\n    <string name=\"nav_search\">Поиск</string>\n    <string name=\"nav_library\">Библиотека</string>\n    <string name=\"nav_profile\">Профиль</string>\n    <string name=\"nav_settings\">Настройки</string>\n    <string name=\"nav_appearance\">Внешний вид</string>\n    <string name=\"manga\">Манга</string>\n    <string name=\"action_retry\">Снова</string>\n    <string name=\"action_ok\">ОК</string>\n    <string name=\"action_cancel\">Отмена</string>\n    <string name=\"action_reset_filter\">Сброс фильтра</string>\n    <string name=\"action_unselect_all\">Снять все выделения</string>\n    <string name=\"action_read_more\">Развернуть</string>\n    <string name=\"action_show_more\">См. ещё</string>\n    <string name=\"action_back\">Назад</string>\n    <string name=\"action_close\">Закрыть</string>\n    <string name=\"action_log_out\">Выйти</string>\n    <string name=\"action_share_profile_url\">Поделиться профилем</string>\n    <string name=\"action_share\">Поделиться</string>\n    <string name=\"action_open_external_short\">Внешние сайты</string>\n    <string name=\"action_remove_from_favorites\">Убрать из избранных</string>\n    <string name=\"action_synchronize\">Синхронизировать</string>\n    <string name=\"action_save_in_gallery\">Сохранить в галерею</string>\n    <string name=\"action_open_in_browser\">Открыть в браузере</string>\n    <string name=\"action_open_on_mal\">Открыть на MyAnimeList.net</string>\n    <string name=\"action_save_changes\">Сохранить изменения</string>\n    <string name=\"action_edit_profile\">Правка профиля</string>\n    <string name=\"action_update_profile\">Обновить профиль</string>\n    <string name=\"action_add\">Добавить</string>\n    <string name=\"action_remove\">Убрать</string>\n    <string name=\"action_update\">Обновление</string>\n    <string name=\"action_start\">Начать</string>\n    <string name=\"action_allow\">Разрешить</string>\n    <string name=\"action_rate\">Оцените</string>\n    <string name=\"action_update_rating\">Обновить рейтинг</string>\n    <string name=\"action_remove_rating\">Убрать рейтинг</string>\n    <string name=\"action_add_profile_link\">Добавить ссылку профиля</string>\n    <string name=\"action_edit_profile_link\">Правка ссылки профиля</string>\n    <string name=\"action_select_profile_link_site\">Выберите сайт</string>\n    <string name=\"hint_search\">Поиск</string>\n    <string name=\"hint_search_characters\">Найти персонажей</string>\n    <string name=\"hint_mark_watched\">Пометить, как смотрели.</string>\n    <string name=\"hint_mark_not_watched\">Пометить, как не смотрел.</string>\n    <string name=\"hint_rating\">Рейтинг</string>\n    <string name=\"dialog_log_out_confirmation\">Вы уверены, что хотите выйти?</string>\n    <string name=\"dialog_remove_from_library_title\">Убрать из библиотеки?</string>\n    <string name=\"dialog_remove_from_library_msg\"><b>%s</b> будет убран из вашей библиотеки.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Разрешение отклонено</string>\n    <string name=\"dialog_request_notification_permission_title\">Разрешить уведомления</string>\n    <string name=\"error_resource_loading\">Не удается загрузить данные.</string>\n    <string name=\"error_nothing_found\">Ничего не найдено.</string>\n    <string name=\"sort_popularity_asc\">Наименее популярный</string>\n    <string name=\"sort_popularity_desc\">Самые популярные</string>\n    <string name=\"sort_average_rating_asc\">Худший рейтинг</string>\n    <string name=\"sort_average_rating_desc\">Лучший рейтинг</string>\n    <string name=\"sort_release_date_asc\">Старейший</string>\n    <string name=\"sort_release_date_desc\">Последний</string>\n    <string name=\"title_character_appearances\">Появления персонажей</string>\n    <string name=\"title_categories\">Категории</string>\n    <string name=\"title_details\">Подробности</string>\n    <string name=\"action_open_external\">Открыть на внешнем сайте</string>\n    <string name=\"action_add_to_favorites\">Добавить в избранное</string>\n    <string name=\"action_share_app_logs\">Поделиться отчётам</string>\n    <string name=\"title_filter\">Фильтр</string>\n    <string name=\"title_description\">Описание</string>\n    <string name=\"action_delete_profile_link\">Удалить ссылку профиля</string>\n    <string name=\"error_image_loading\">Не удалось загрузить изображение.</string>\n    <string name=\"error_something_wrong\">Опс, что-то пошло не так!</string>\n    <string name=\"error_requires_notification_permission\">Требуется разрешение на уведомление.</string>\n    <string name=\"anime\">Аниме</string>\n    <string name=\"action_read_less\">Свернуть</string>\n    <string name=\"action_show_less\">Показать далее</string>\n    <string name=\"action_view\">Вид</string>\n    <string name=\"action_dismiss\">Отклонить</string>\n    <string name=\"dialog_notification_permission_rejected\">Вы не будете получать уведомления и не будете информированы об обновлениях приложения.</string>\n    <string name=\"error_mapping_loading\">Не удалось загрузить сопоставления для внешних сайтов.</string>\n    <string name=\"error_invalid_user\">Неизвестный пользователь. Вы вошли в систему?</string>\n    <string name=\"error_user_update_failed\">Не удалось обновить данные пользователя.</string>\n    <string name=\"error_user_delete_waifu_failed\">Не удалось удалить Waifu/Husbando.</string>\n    <string name=\"error_user_update_image_failed\">Не удалось обновить изображение профиля или обложку.</string>\n    <string name=\"error_user_create_profile_link_failed\">Не удалось создать ссылку на профиль для %s.</string>\n    <string name=\"error_user_update_profile_link_failed\">Не удалось обновить ссылку на профиль для %s.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Не удалось удалить ссылку на профиль для %s.</string>\n    <string name=\"error_requires_external_storage_permission\">Требуется разрешение на запись во внешнее хранилище.</string>\n    <string name=\"error_search_provider_unavailable\">Поисковый провайдер недоступен.</string>\n    <string name=\"error_library_update_failed\">Не удалось обновить запись в библиотеке.</string>\n    <string name=\"error_library_update_failed_multiple\">Не удалось обновить %d записей в библиотеке.</string>\n    <string name=\"error_library_update_not_found\">Запись в библиотеке не существует.</string>\n    <string name=\"error_library_add_failed\">Не удалось добавить в вашу библиотеку.</string>\n    <string name=\"error_library_delete_failed\">Не удалось удалить запись из библиотеки.</string>\n    <string name=\"title_media_type\">Тип медиа</string>\n    <string name=\"title_streamer\">Ссылки на трансляцию</string>\n    <string name=\"title_ratings\">Рейтинги</string>\n    <string name=\"title_more_franchise\">Ещё из этой серии</string>\n    <string name=\"title_favorite_anime\">Любимые аниме</string>\n    <string name=\"title_favorite_manga\">Любимые манги</string>\n    <string name=\"title_favorite_characters\">Любимые персонажи</string>\n    <string name=\"title_authentication\">Аутентификация</string>\n    <string name=\"title_login\">Войдите в свой аккаунт Kitsu</string>\n    <string name=\"title_play_trailer\">Смотреть трейлер</string>\n    <string name=\"title_about_me\">Обо мне</string>\n    <string name=\"title_episodes\">Эпизоды</string>\n    <string name=\"title_chapters\">Главы</string>\n    <string name=\"title_characters\">Персонажи</string>\n    <string name=\"title_edit_library_entry\">Правка записи в библиотеке</string>\n    <string name=\"title_edit_profile\">Правка профиля</string>\n    <string name=\"no_information\">Нет данных</string>\n    <string name=\"no_data_available\">Данные отсутствуют</string>\n    <string name=\"library_status_watching\">Смотреть</string>\n    <string name=\"library_status_completed\">Завершено</string>\n    <string name=\"library_status_planned_manga\">Хотите прочитать</string>\n    <string name=\"status_current\">Уже в эфире</string>\n    <string name=\"status_current_manga\">Публикация</string>\n    <string name=\"status_tba\">TBA (Будет объявлено позже)</string>\n    <string name=\"status_finished\">Завершен</string>\n    <string name=\"status_unreleased\">Не выпущено</string>\n    <string name=\"status_upcoming\">Предстоящие</string>\n    <string name=\"character_role_main\">Главный</string>\n    <string name=\"character_role_supporting\">Второстепенный</string>\n    <string name=\"character_role_recurring\">Периодический</string>\n    <string name=\"library_status_dropped\">Брошено</string>\n    <string name=\"library_status_reading\">Читаю</string>\n    <string name=\"library_action_remove\">Убрать из библиотеки</string>\n    <string name=\"library_action_add\">Добавить в библиотеку</string>\n    <string name=\"library_action_edit\">Изменить запись в библиотеке</string>\n    <string name=\"data_length_total\">%s всего</string>\n    <string name=\"data_title_english\">Английский</string>\n    <string name=\"data_title_japanese\">Японский</string>\n    <string name=\"data_title_japanese_romaji\">Японский (Ромадзи)</string>\n    <string name=\"data_abbreviated_titles\">Синонимы</string>\n    <string name=\"data_other_names\">Также известен как</string>\n    <string name=\"data_title_type\">Тип</string>\n    <string name=\"data_title_status\">Состояние</string>\n    <string name=\"data_title_published\">Опубликовано</string>\n    <string name=\"data_rank\">Ранг #%d</string>\n    <string name=\"data_popularity\">Популярность #%d</string>\n    <string name=\"data_kitsu_score\">Оценка на Kitsu %s%%</string>\n    <string name=\"data_length_each\">%d минут каждая</string>\n    <string name=\"data_title_aired\">Выпущено</string>\n    <string name=\"data_title_tba\">Ожидается</string>\n    <string name=\"data_title_season\">Сезон</string>\n    <string name=\"data_title_age_rating\">Рейтинг</string>\n    <string name=\"data_title_serialization\">Сериализация</string>\n    <string name=\"data_title_chapters\">Главы</string>\n    <string name=\"data_title_volumes\">Тома</string>\n    <string name=\"data_title_episodes\">Эпизоды</string>\n    <string name=\"data_title_length\">Длина</string>\n    <string name=\"data_title_producers\">Продюсеры</string>\n    <string name=\"data_title_studios\">Студии</string>\n    <string name=\"relationship_prequel\">Предыстория</string>\n    <string name=\"data_title_licensors\">Лицензиары</string>\n    <string name=\"season_fall\">Осень</string>\n    <string name=\"relationship_sequel\">Продолжение</string>\n    <string name=\"season_summer\">Лето</string>\n    <string name=\"relationship_adaptation\">Адаптация</string>\n    <string name=\"relationship_character\">Персонаж</string>\n    <string name=\"relationship_other\">Другие</string>\n    <string name=\"section_trending\">Популярное на этой неделе</string>\n    <string name=\"relationship_full_story\">Полная история</string>\n    <string name=\"relationship_side_story\">Другая история</string>\n    <string name=\"relationship_spinoff\">Ответвление</string>\n    <string name=\"section_top_airing_anime\">Лучшие аниме в эфире</string>\n    <string name=\"section_top_upcoming_anime\">Лучшие ожидаемые аниме</string>\n    <string name=\"section_highest_rated_anime\">Высоко оцененные аниме</string>\n    <string name=\"section_most_popular_anime\">Самые популярные аниме</string>\n    <string name=\"section_top_upcoming_manga\">Лучшие ожидаемые манга</string>\n    <string name=\"section_highest_rated_manga\">Высоко оцененные манга</string>\n    <string name=\"section_most_popular_manga\">Самые популярные манга</string>\n    <string name=\"preference_category_ui\">Интерфейс</string>\n    <string name=\"preference_category_account\">Аккаунт</string>\n    <string name=\"preference_category_advanced\">Расширенный</string>\n    <string name=\"preference_appearance_description\">Смените тему и включите темный режим.</string>\n    <string name=\"preference_dark_mode\">Тёмный режим</string>\n    <string name=\"preference_category_about\">Об</string>\n    <string name=\"preference_dark_mode_light\">Светлая</string>\n    <string name=\"preference_dark_mode_dark\">Тёмная</string>\n    <string name=\"preference_dark_mode_follow_system\">Как в системе</string>\n    <string name=\"preference_dark_mode_battery_saver\">Экономия батареи</string>\n    <string name=\"preference_oled_black_mode\">OLED-чёрный</string>\n    <string name=\"preference_oled_black_mode_description\">Использовать черный фон в темном режиме.</string>\n    <string name=\"preference_app_theme\">Тема</string>\n    <string name=\"preference_dynamic_color_theme\">Использовать динамический цвет</string>\n    <string name=\"preference_dynamic_color_theme_description\">Адаптироваться к системной теме.</string>\n    <string name=\"preference_app_theme_default\">Базовый</string>\n    <string name=\"preference_app_theme_purple\">Лиловый</string>\n    <string name=\"preference_app_theme_green\">Зеленый</string>\n    <string name=\"preference_media_item_size\">Размер изображения плаката</string>\n    <string name=\"preference_media_item_size_small\">Маленький</string>\n    <string name=\"preference_media_item_size_large\">Большой</string>\n    <string name=\"preference_start_fragment\">Страница по умолчанию</string>\n    <string name=\"preference_start_fragment_home\">Главная</string>\n    <string name=\"preference_start_fragment_search\">Поиск</string>\n    <string name=\"preference_start_fragment_library\">Библиотека</string>\n    <string name=\"preference_start_fragment_profile\">Профиль</string>\n    <string name=\"preference_start_fragment_description\">Страница по умолчанию имеет значение %s.</string>\n    <string name=\"preference_language_default\">Язык по умолчанию</string>\n    <string name=\"preference_language\">Язык</string>\n    <string name=\"preference_display_name\">Отображаемое имя</string>\n    <string name=\"preference_profile_url\">URL-адрес профиля</string>\n    <string name=\"preference_profile_url_not_set\">URL-адрес профиля не задан.</string>\n    <string name=\"preference_country\">Страна</string>\n    <string name=\"preference_titles\">Тайтлы</string>\n    <string name=\"preference_country_summary\">Страна имеет значение %s</string>\n    <string name=\"preference_country_summary_non\">Страна не указана.</string>\n    <string name=\"preference_titles_canonical\">Канонический</string>\n    <string name=\"preference_titles_romanized\">Романизированный</string>\n    <string name=\"preference_titles_english\">Английский</string>\n    <string name=\"preference_adult_content\">Материал для взрослых</string>\n    <string name=\"preference_adult_content_description_sfw\">Материалы с возрастным рейтингом R18+ будут скрыты.</string>\n    <string name=\"preference_adult_content_description_sometimes\">Материалы с возрастным рейтингом R18+ будут видны, но сообщения с пометкой NSFW будут скрыты в глобальной ленте.</string>\n    <string name=\"preference_adult_content_description_everywhere\">Материалы с возрастным рейтингом R18+ будут видны повсюду.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Скрыть содержимое для взрослых</string>\n    <string name=\"preference_titles_description\">Определите, как будут отображаться медиа тайтлов.</string>\n    <string name=\"preference_sfw_filter_show_all\">Взрослые материалы повсюду</string>\n    <string name=\"preference_rating_system\">Рейтинговая система</string>\n    <string name=\"preference_rating_system_simple\">Простой</string>\n    <string name=\"preference_rating_system_regular\">Регулярный</string>\n    <string name=\"preference_rating_system_advanced\">Расширенный</string>\n    <string name=\"preference_remember_search_filters\">Запомнить фильтры поиска</string>\n    <string name=\"preference_remember_search_filters_description\">Применить последние использованные фильтры поиска после перезапуска приложения.</string>\n    <string name=\"preference_app_version\">Об версии</string>\n    <string name=\"preference_app_version_description\">Нажмите для проверки обновления.</string>\n    <string name=\"preference_github_repo\">Репозиторий на GitHub</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Используйте системный выбор файлов вместо нового выбора фото.</string>\n    <string name=\"preference_check_for_updates\">Проверять на обновления при запуске</string>\n    <string name=\"preference_check_for_updates_description\">Проверять наличие новой версии приложения при каждом запуске.</string>\n    <string name=\"preference_not_logged_in\">Вы должны войти в систему, чтобы изменить эту настройку.</string>\n    <string name=\"prompt_email\">Электронная почта</string>\n    <string name=\"prompt_password\">Пароль</string>\n    <string name=\"action_log_in_to_kitsu\">Вход в Kitsu</string>\n    <string name=\"action_log_in\">Вход</string>\n    <string name=\"action_sign_in\">Войти</string>\n    <string name=\"login_failed\">Вход не удался</string>\n    <string name=\"status_logging_in\">Вход в систему…</string>\n    <string name=\"logged_in_success\">Вход выполнен как %s.</string>\n    <string name=\"login_no_account\">У вас нет аккаунта? Создайте его на <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"not_logged_in\">Вход не выполнен</string>\n    <string name=\"library_not_started\">Не начато</string>\n    <string name=\"library_not_rated\">Не оценено</string>\n    <string name=\"invalid_username\">Недействительный адрес электронной почты</string>\n    <string name=\"library_kind_all\">Все</string>\n    <string name=\"library_not_synchronized\">Не синхронизировано</string>\n    <string name=\"library_edit_status\">Состояние библиотеки</string>\n    <string name=\"library_edit_progress\">Прогресс</string>\n    <string name=\"library_edit_volumes\">Тома</string>\n    <string name=\"library_edit_rating\">Рейтинг</string>\n    <string name=\"library_not_logged_in_title\">Вход в систему для доступа к библиотеке</string>\n    <string name=\"library_edit_rewatch_count\">Кол-во просмотров</string>\n    <string name=\"library_edit_reread_count\">Кол-во перечитанных книг</string>\n    <string name=\"library_edit_start_reread\">Начать повторное чтение</string>\n    <string name=\"library_edit_start_rewatch\">Начать повторный просмотр</string>\n    <string name=\"library_edit_privacy\">Конфиденциальность</string>\n    <string name=\"library_edit_privacy_public\">Публичный</string>\n    <string name=\"library_edit_privacy_private\">Частный</string>\n    <string name=\"library_edit_started\">Начато</string>\n    <string name=\"library_edit_no_date_set\">Дата не указана</string>\n    <string name=\"library_edit_notes\">Личные заметки</string>\n    <string name=\"info_log_in_required\">Войдите, чтобы получить доступ ко всем функциям.</string>\n    <string name=\"info_update_checking_new_version\">Проверка новой версии…</string>\n    <string name=\"profile_stats_manga_chapters_read\">Прочитано %d глав.</string>\n    <string name=\"filter_year\">Год</string>\n    <string name=\"filter_avg_rating\">Средний рейтинг</string>\n    <string name=\"filter_select_categories\">Выберите категории</string>\n    <string name=\"filter_season\">Сезон</string>\n    <string name=\"filter_subtype\">Подтип</string>\n    <string name=\"filter_age_rating\">Возрастной рейтинг</string>\n    <string name=\"info_update_new_version_available_text\">Доступна версия %s Kitsune.</string>\n    <string name=\"info_update_no_new_version_available\">Новая версия отсутствует.</string>\n    <string name=\"info_update_failed\">Не удалось проверить новую версию.</string>\n    <string name=\"info_logged_out_token_expired\">Ваш токен доступа истек. Пожалуйста, войдите снова.</string>\n    <string name=\"profile_not_logged_in_title\">Ничего не видно!</string>\n    <string name=\"profile_manga_stats\">Статистика манги</string>\n    <string name=\"filter_streamers\">Стримеры</string>\n    <string name=\"profile_cover_image_description\">Обложка</string>\n    <string name=\"profile_avatar_image_description\">Картинка профиля</string>\n    <string name=\"profile_link_url\">URL-адрес или имя пользователя</string>\n    <string name=\"notification_updates\">Обновления приложения</string>\n    <string name=\"profile_data_birthday\">Дата рождения</string>\n    <string name=\"profile_data_join_date\">Дата вступления</string>\n    <string name=\"profile_gender_male\">Мужской</string>\n    <string name=\"profile_gender_female\">Женский</string>\n    <string name=\"profile_gender_custom_hint\">Укажите ваш пол</string>\n    <string name=\"profile_waifu_or_husbando\">Жена или муж</string>\n    <string name=\"unit_chapter\">Глава %d</string>\n    <string name=\"unit_episode\">Эпизод %d</string>\n    <string name=\"section_top_airing_manga\">Лучшие издания манги</string>\n    <string name=\"preference_app_theme_blue\">Синий</string>\n    <string name=\"preference_media_item_size_medium\">Средний</string>\n    <string name=\"library_not_logged_in_text\">Войдите в свой аккаунт Kitsu, чтобы управлять библиотекой и получить доступ ко всем функциям.</string>\n    <string name=\"library_edit_finished\">Окончено</string>\n    <string name=\"info_update_new_version_available\">Доступна новая версия.</string>\n    <string name=\"info_image_saved_in_gallery\">Изображение успешно сохранено в галерее.</string>\n    <string name=\"library_status_planned\">Хочу посмотреть</string>\n    <string name=\"library_status_on_hold\">Приостановлено</string>\n    <string name=\"character_role_cameo\">Камэо</string>\n    <string name=\"season_spring\">Весна</string>\n    <string name=\"season_winter\">Зима</string>\n    <string name=\"relationship_parent_story\">Основная история</string>\n    <string name=\"profile_not_logged_in_text\">Войдите в свой аккаунт Kitsu или создайте новый аккаунт, чтобы получить доступ ко всем функциям.</string>\n    <string name=\"profile_anime_stats\">Статистика аниме</string>\n    <string name=\"app_logs_no_data\">Нет доступных записей.</string>\n    <string name=\"nav_os_libraries\">Библиотеки с открытым кодом</string>\n    <string name=\"dialog_request_notification_permission\">Получать уведомления о выпуске новой версии приложения.</string>\n    <string name=\"relationship_alternative_setting\">Доп. настройки</string>\n    <string name=\"relationship_alternative_version\">Альтернативная версия</string>\n    <string name=\"relationship_summary\">Сводка</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Ограничить лентой подписок</string>\n    <string name=\"profile_data_gender\">Пол</string>\n    <string name=\"widget_empty_action\">Открыть библиотеку</string>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s месяц</item>\n        <item quantity=\"few\">%s месяца</item>\n        <item quantity=\"many\">%s месяцев</item>\n        <item quantity=\"other\">%s месяцев</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s год</item>\n        <item quantity=\"few\">%s года</item>\n        <item quantity=\"many\">%s лет</item>\n        <item quantity=\"other\">%s лет</item>\n    </plurals>\n    <string name=\"action_next\">Далее</string>\n    <string name=\"action_create_account\">Завести учётную запись</string>\n    <string name=\"profile_stats_time_spent_total\">%s всего.</string>\n    <string name=\"profile_stats_completed\">%1$d завершено. Более <b>%2$d%%</b> пользователей.</string>\n    <string name=\"profile_data_location\">Локация</string>\n    <string name=\"profile_data_private\">Это секрет.</string>\n    <string name=\"profile_gender_custom\">Ручной</string>\n    <string name=\"profile_find_waifu\">Найдите своего особого человека</string>\n    <string name=\"onboarding_welcome_title\">Добро пожаловать в Kitsune!</string>\n    <string name=\"onboarding_welcome_subtitle\">Ваш личный компаньон по аниме и манге.</string>\n    <string name=\"onboarding_welcome_feature_search\">Поиск</string>\n    <string name=\"onboarding_welcome_feature_explore\">Проводник</string>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s час</item>\n        <item quantity=\"few\">%s часа</item>\n        <item quantity=\"many\">%s часов</item>\n        <item quantity=\"other\">%s часов</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s день</item>\n        <item quantity=\"few\">%s дня</item>\n        <item quantity=\"many\">%s дней</item>\n        <item quantity=\"other\">%s дней</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s минута</item>\n        <item quantity=\"few\">%s минуты</item>\n        <item quantity=\"many\">%s минут</item>\n        <item quantity=\"other\">%s минут</item>\n    </plurals>\n    <string name=\"action_continue\">Продолжить</string>\n    <string name=\"action_skip\">Пропустить</string>\n    <string name=\"preference_force_legacy_image_picker\">Использовать старый выбор фотографий</string>\n    <string name=\"preference_app_logs_description\">Смотрите и делитесь сообщениями журнала приложения.</string>\n    <string name=\"preference_open_source_libraries_description\">Список всех используемых библиотек с открытым исходным кодом.</string>\n    <string name=\"profile_stats_anime_watch_time\">%s потрачено на просмотр аниме.</string>\n    <string name=\"profile_data_waifu\">Вайфу</string>\n    <string name=\"profile_data_join_date_ago\">%s назад</string>\n    <string name=\"profile_data_husbando\">Хусбандо</string>\n    <string name=\"profile_data_bio\">Биография</string>\n    <string name=\"widget_description\">Прогресс библиотеки</string>\n    <string name=\"widget_empty_text\">В вашей библиотеке нет активного отслеживаемого объекта</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Найдите свое любимое аниме и мангу.</string>\n    <string name=\"nav_app_logs\">Журналы приложения</string>\n    <string name=\"filter_kind\">Тип</string>\n    <string name=\"onboarding_welcome_feature_track\">Трек</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Следите за тем, что вы смотрели и читали.</string>\n    <string name=\"onboarding_welcome_action\">Начать</string>\n    <string name=\"onboarding_login_title\">Войти в Kitsu</string>\n    <string name=\"onboarding_login_is_logged_in\">Вы вошли в систему</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Завести учётную запись</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Вы будете перенаправлены на %1$s, где сможете создать новый аккаунт.</string>\n    <string name=\"onboarding_setup_subtitle\">Для начала работы установите желаемые параметры. Их всегда можно изменить позже в настройках.</string>\n    <string name=\"onboarding_setup_title\">Первоначальная установка</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">Требуется разрешение на уведомление.</string>\n    <string name=\"onboarding_setup_updates\">Проверьте обновления</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Откройте для себя новые и популярные тайтлы.</string>\n    <string name=\"onboarding_login_subtitle\">Войдите в свой аккаунт Kitsu или создайте новый, чтобы получить доступ ко всем функциям.</string>\n    <string name=\"onboarding_setup_updates_description\">Получать уведомление когда новый выпуск доступен на GitHub.</string>\n    <string name=\"onboarding_setup_title_language_selected\">Выбрано: %1$s</string>\n    <string name=\"onboarding_setup_action\">Завершить установку</string>\n    <string name=\"onboarding_setup_title_language_description\">Выберите ваш предпочтительный язык для тайтлов.</string>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s секунда</item>\n        <item quantity=\"few\">%s секунды</item>\n        <item quantity=\"many\">%s секунд</item>\n        <item quantity=\"other\">%s секунд</item>\n    </plurals>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s страница</item>\n        <item quantity=\"few\">%s страницы</item>\n        <item quantity=\"many\">%s страниц</item>\n        <item quantity=\"other\">%s страниц</item>\n    </plurals>\n    <string name=\"onboarding_setup_title_language\">Язык тайтлов</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ta/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"action_open_external\">வெளிப்புற தளத்தில் திறக்கவும்</string>\n    <string name=\"action_open_external_short\">வெளிப்புற தளங்கள்</string>\n    <string name=\"hint_search\">தேடல்</string>\n    <string name=\"title_edit_profile\">சுயவிவரத்தைத் திருத்து</string>\n    <string name=\"no_information\">தெரியவில்லை</string>\n    <string name=\"status_current_manga\">வெளியீடு</string>\n    <string name=\"status_tba\">Tba</string>\n    <string name=\"season_spring\">வேனில்</string>\n    <string name=\"preference_dark_mode_follow_system\">அமைப்பைப் பின்தொடரவும்</string>\n    <string name=\"preference_titles\">தலைப்புகள்</string>\n    <string name=\"preference_remember_search_filters_description\">பயன்பாடு மறுதொடக்கம் செய்த பிறகு கடைசியாக பயன்படுத்தப்பட்ட தேடல் வடிப்பான்களைப் பயன்படுத்துங்கள்.</string>\n    <string name=\"profile_stats_time_spent_total\">மொத்தம் %s.</string>\n    <string name=\"widget_empty_action\">திறந்த நூலகம்</string>\n    <string name=\"onboarding_login_is_logged_in\">நீங்கள் உள்நுழைந்துள்ளீர்கள்</string>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s மணித்துளி</item>\n        <item quantity=\"other\">%s நிமிடங்கள்</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s மணிநேரம்</item>\n        <item quantity=\"other\">%s மணிநேரங்கள்</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s நாள்</item>\n        <item quantity=\"other\">%s நாட்கள்</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s மாதம்</item>\n        <item quantity=\"other\">%s மாதங்கள்</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s ஆண்டு</item>\n        <item quantity=\"other\">%s ஆண்டுகள்</item>\n    </plurals>\n    <string name=\"nav_home\">வீடு</string>\n    <string name=\"nav_search\">தேடல்</string>\n    <string name=\"nav_library\">நூலகம்</string>\n    <string name=\"nav_profile\">சுயவிவரம்</string>\n    <string name=\"nav_settings\">அமைப்புகள்</string>\n    <string name=\"nav_appearance\">தோற்றம்</string>\n    <string name=\"nav_app_logs\">பயன்பாட்டு பதிவுகள்</string>\n    <string name=\"nav_os_libraries\">திறந்த மூல நூலகங்கள்</string>\n    <string name=\"anime\">அனிம்</string>\n    <string name=\"manga\">மங்கா</string>\n    <string name=\"action_retry\">மீண்டும் முயற்சிக்கவும்</string>\n    <string name=\"action_ok\">சரி</string>\n    <string name=\"action_cancel\">ரத்துசெய்</string>\n    <string name=\"action_reset_filter\">வடிகட்டியை மீட்டமை</string>\n    <string name=\"action_unselect_all\">அனைத்தையும் தேர்வு செய்யுங்கள்</string>\n    <string name=\"action_read_more\">மேலும் வாசிக்க</string>\n    <string name=\"action_read_less\">குறைவாகப் படியுங்கள்</string>\n    <string name=\"action_show_more\">மேலும் காட்டு</string>\n    <string name=\"action_show_less\">குறைவாகக் காட்டு</string>\n    <string name=\"action_view\">பார்வை</string>\n    <string name=\"action_back\">பின்</string>\n    <string name=\"action_skip\">தவிர்</string>\n    <string name=\"action_next\">அடுத்தது</string>\n    <string name=\"action_continue\">தொடரவும்</string>\n    <string name=\"action_close\">மூடு</string>\n    <string name=\"action_log_out\">விடுபதிகை</string>\n    <string name=\"action_share_profile_url\">சுயவிவரத்தைப் பகிரவும்</string>\n    <string name=\"action_share\">பங்கு</string>\n    <string name=\"action_share_app_logs\">பதிவுகளைப் பகிரவும்</string>\n    <string name=\"action_add_to_favorites\">பிடித்தவைகளில் சேர்க்கவும்</string>\n    <string name=\"action_remove_from_favorites\">பிடித்தவைகளிலிருந்து அகற்று</string>\n    <string name=\"action_dismiss\">தள்ளுபடி</string>\n    <string name=\"action_synchronize\">ஒத்திசைக்கவும்</string>\n    <string name=\"action_save_in_gallery\">கேலரியில் சேமிக்கவும்</string>\n    <string name=\"action_open_in_browser\">உலாவியில் திற</string>\n    <string name=\"action_open_on_mal\">Mianimelist.net இல் திறக்கவும்</string>\n    <string name=\"action_save_changes\">மாற்றங்களைச் சேமிக்கவும்</string>\n    <string name=\"action_edit_profile\">சுயவிவரத்தைத் திருத்து</string>\n    <string name=\"action_update_profile\">சுயவிவரத்தைப் புதுப்பிக்கவும்</string>\n    <string name=\"action_add\">கூட்டு</string>\n    <string name=\"action_remove\">அகற்று</string>\n    <string name=\"action_update\">புதுப்பிப்பு</string>\n    <string name=\"action_start\">தொடங்கு</string>\n    <string name=\"action_allow\">இசைவு</string>\n    <string name=\"action_rate\">விகிதம்</string>\n    <string name=\"action_update_rating\">புதுப்பிப்பு மதிப்பீடு</string>\n    <string name=\"action_remove_rating\">மதிப்பீட்டை அகற்று</string>\n    <string name=\"action_add_profile_link\">சுயவிவர இணைப்பைச் சேர்க்கவும்</string>\n    <string name=\"action_edit_profile_link\">சுயவிவர இணைப்பைத் திருத்தவும்</string>\n    <string name=\"action_select_profile_link_site\">ஒரு தளத்தைத் தேர்ந்தெடுக்கவும்</string>\n    <string name=\"action_delete_profile_link\">சுயவிவர இணைப்பை நீக்கு</string>\n    <string name=\"hint_search_characters\">எழுத்துக்களைத் தேடுங்கள்</string>\n    <string name=\"hint_mark_watched\">பார்த்தபடி குறி.</string>\n    <string name=\"hint_mark_not_watched\">பார்க்காதபடி குறி.</string>\n    <string name=\"hint_rating\">செயல்வரம்பு</string>\n    <string name=\"dialog_log_out_confirmation\">நீங்கள் நிச்சயமாக வெளியேற விரும்புகிறீர்களா?</string>\n    <string name=\"dialog_remove_from_library_title\">நூலகத்திலிருந்து அகற்றவா?</string>\n    <string name=\"sort_average_rating_desc\">சிறந்த மதிப்பிடப்பட்ட</string>\n    <string name=\"sort_release_date_asc\">பழமையானது</string>\n    <string name=\"sort_release_date_desc\">அண்மைக் கால</string>\n    <string name=\"title_media_type\">ஊடக வகை</string>\n    <string name=\"title_character_appearances\">எழுத்து தோற்றங்கள்</string>\n    <string name=\"title_categories\">வகைகள்</string>\n    <string name=\"title_filter\">வடிப்பி</string>\n    <string name=\"title_description\">விவரம்</string>\n    <string name=\"title_details\">விவரங்கள்</string>\n    <string name=\"title_streamer\">ச்ட்ரீமிங் இணைப்புகள்</string>\n    <string name=\"title_ratings\">மதிப்பீடுகள்</string>\n    <string name=\"title_more_franchise\">இந்த தொடரிலிருந்து மேலும்</string>\n    <string name=\"title_favorite_anime\">பிடித்த அனிம்</string>\n    <string name=\"title_favorite_manga\">மங்காவுக்கு ஆதரவாக</string>\n    <string name=\"title_favorite_characters\">பிடித்த எழுத்துக்கள்</string>\n    <string name=\"title_authentication\">ஏற்பு</string>\n    <string name=\"title_login\">உங்கள் கிட்சு கணக்கில் உள்நுழைக</string>\n    <string name=\"title_play_trailer\">டிரெய்லர் விளையாடுங்கள்</string>\n    <string name=\"title_about_me\">என்னைப் பற்றி</string>\n    <string name=\"title_episodes\">அத்தியாயங்கள்</string>\n    <string name=\"title_chapters\">பாடங்கள்</string>\n    <string name=\"title_characters\">எழுத்துக்கள்</string>\n    <string name=\"title_edit_library_entry\">நூலக நுழைவைத் திருத்தவும்</string>\n    <string name=\"no_data_available\">தரவு எதுவும் கிடைக்கவில்லை</string>\n    <string name=\"status_current\">தற்போது ஒளிபரப்பாகிறது</string>\n    <string name=\"status_finished\">முடிந்தது</string>\n    <string name=\"status_unreleased\">வெளியிடப்பட்டாதது</string>\n    <string name=\"status_upcoming\">வரவிருக்கும்</string>\n    <string name=\"character_role_main\">முக்கிய</string>\n    <string name=\"character_role_supporting\">துணை</string>\n    <string name=\"character_role_recurring\">மறுநிகழும்</string>\n    <string name=\"library_status_watching\">பார்ப்பது</string>\n    <string name=\"character_role_cameo\">கேமியோ</string>\n    <string name=\"library_status_planned\">பார்க்க விரும்புகிறேன்</string>\n    <string name=\"library_status_completed\">முடிந்தது</string>\n    <string name=\"library_status_on_hold\">நிறுத்தி</string>\n    <string name=\"library_status_dropped\">கைவிடப்பட்டது</string>\n    <string name=\"library_status_reading\">படித்தல்</string>\n    <string name=\"library_status_planned_manga\">படிக்க விரும்புகிறேன்</string>\n    <string name=\"library_action_remove\">நூலகத்திலிருந்து அகற்று</string>\n    <string name=\"library_action_add\">நூலகத்தில் சேர்க்கவும்</string>\n    <string name=\"library_action_edit\">நூலக நுழைவைத் திருத்தவும்</string>\n    <string name=\"data_rank\">தரவரிசை #%d</string>\n    <string name=\"data_popularity\">புகழ் #%d</string>\n    <string name=\"data_kitsu_score\">கிட்சு கள் இந்த %s%%</string>\n    <string name=\"data_length_total\">%s மொத்தம்</string>\n    <string name=\"data_length_each\">ஒவ்வொன்றும் %d நிமிடங்கள்</string>\n    <string name=\"data_title_english\">ஆங்கிலம்</string>\n    <string name=\"data_title_japanese\">சப்பானியர்கள்</string>\n    <string name=\"data_title_japanese_romaji\">சப்பானிய (ராம்சி)</string>\n    <string name=\"data_abbreviated_titles\">ஒத்த</string>\n    <string name=\"data_title_type\">வகை</string>\n    <string name=\"data_other_names\">என்றும் அழைக்கப்படுகிறது</string>\n    <string name=\"data_title_status\">நிலை</string>\n    <string name=\"data_title_aired\">ஒளிபரப்பப்பட்டது</string>\n    <string name=\"data_title_published\">வெளியிடப்பட்டது</string>\n    <string name=\"data_title_tba\">எதிர்பார்க்கப்படுகிறது</string>\n    <string name=\"data_title_season\">சீசன்</string>\n    <string name=\"data_title_serialization\">சீரியலைசேசன்</string>\n    <string name=\"data_title_chapters\">பாடங்கள்</string>\n    <string name=\"data_title_volumes\">தொகுதிகள்</string>\n    <string name=\"data_title_episodes\">அத்தியாயங்கள்</string>\n    <string name=\"data_title_age_rating\">செயல்வரம்பு</string>\n    <string name=\"data_title_length\">நீளம்</string>\n    <string name=\"data_title_producers\">தயாரிப்பாளர்கள்</string>\n    <string name=\"data_title_licensors\">உரிமதாரர்கள்</string>\n    <string name=\"data_title_studios\">ச்டுடியோச்</string>\n    <string name=\"season_winter\">குளிர்காலம்</string>\n    <string name=\"season_summer\">கோடை காலம்</string>\n    <string name=\"season_fall\">வீழ்ச்சி</string>\n    <string name=\"relationship_sequel\">அதன் தொடர்ச்சி</string>\n    <string name=\"relationship_prequel\">முன்னுரை</string>\n    <string name=\"relationship_summary\">சுருக்கம்</string>\n    <string name=\"relationship_full_story\">முழு கதை</string>\n    <string name=\"relationship_spinoff\">ச்பின்-ஆஃப்</string>\n    <string name=\"relationship_adaptation\">தழுவல்</string>\n    <string name=\"relationship_character\">எழுத்து</string>\n    <string name=\"relationship_other\">மற்றொன்று</string>\n    <string name=\"section_trending\">இந்த வாரம் பிரபலமானது</string>\n    <string name=\"section_top_airing_anime\">மேலே ஒளிபரப்பப்படும் அனிமேசன்</string>\n    <string name=\"section_top_upcoming_anime\">வரவிருக்கும் அனிமேசன்</string>\n    <string name=\"section_highest_rated_anime\">அதிக மதிப்பிடப்பட்ட அனிமேசன்</string>\n    <string name=\"relationship_alternative_setting\">ஆல்ட். அமைத்தல்</string>\n    <string name=\"relationship_alternative_version\">ஆல்ட். பதிப்பு</string>\n    <string name=\"relationship_side_story\">பக்க கதை</string>\n    <string name=\"relationship_parent_story\">பெற்றோர் கதை</string>\n    <string name=\"section_most_popular_anime\">மிகவும் பிரபலமான அனிம்</string>\n    <string name=\"section_top_airing_manga\">சிறந்த வெளியீட்டு மங்கா</string>\n    <string name=\"section_top_upcoming_manga\">வரவிருக்கும் மங்கா</string>\n    <string name=\"section_highest_rated_manga\">அதிக மதிப்பிடப்பட்ட மங்கா</string>\n    <string name=\"section_most_popular_manga\">மிகவும் பிரபலமான மங்கா</string>\n    <string name=\"preference_category_ui\">பயனர் இடைமுகம்</string>\n    <string name=\"preference_category_account\">கணக்கு</string>\n    <string name=\"preference_category_advanced\">மேம்பட்ட</string>\n    <string name=\"preference_category_about\">பற்றி</string>\n    <string name=\"preference_appearance_description\">கருப்பொருளை மாற்றி இருண்ட பயன்முறையை மாற்றவும்.</string>\n    <string name=\"preference_dark_mode\">இருண்ட முறை</string>\n    <string name=\"preference_dark_mode_light\">ஒளி</string>\n    <string name=\"preference_dark_mode_dark\">இருண்ட</string>\n    <string name=\"preference_dark_mode_battery_saver\">பேட்டரி சேமிப்பாளர்</string>\n    <string name=\"preference_oled_black_mode\">OLED கருப்பு</string>\n    <string name=\"preference_oled_black_mode_description\">இருண்ட பயன்முறையில் கருப்பு பின்னணியைப் பயன்படுத்தவும்.</string>\n    <string name=\"preference_app_theme\">கருப்பொருள்</string>\n    <string name=\"preference_dynamic_color_theme\">மாறும் வண்ணத்தைப் பயன்படுத்துங்கள்</string>\n    <string name=\"preference_dynamic_color_theme_description\">கணினி கருப்பொருளுக்கு ஏற்ப.</string>\n    <string name=\"preference_app_theme_default\">இயல்புநிலை</string>\n    <string name=\"preference_app_theme_purple\">ஊதா</string>\n    <string name=\"preference_app_theme_blue\">நீலம்</string>\n    <string name=\"preference_app_theme_green\">பச்சை</string>\n    <string name=\"preference_media_item_size\">சுவரொட்டி பட அளவு</string>\n    <string name=\"preference_media_item_size_small\">சிறிய</string>\n    <string name=\"preference_media_item_size_medium\">சராசரி</string>\n    <string name=\"preference_media_item_size_large\">பெரிய</string>\n    <string name=\"preference_start_fragment\">இயல்புநிலை பக்கம்</string>\n    <string name=\"preference_start_fragment_home\">வீடு</string>\n    <string name=\"preference_start_fragment_search\">தேடல்</string>\n    <string name=\"preference_start_fragment_library\">நூலகம்</string>\n    <string name=\"preference_start_fragment_profile\">சுயவிவரம்</string>\n    <string name=\"library_edit_finished\">முடிந்தது</string>\n    <string name=\"library_edit_no_date_set\">தேதி அமைக்கப்படவில்லை</string>\n    <string name=\"library_edit_notes\">தனிப்பட்ட குறிப்புகள்</string>\n    <string name=\"info_log_in_required\">அனைத்து அம்சங்களையும் அணுக உள்நுழைக.</string>\n    <string name=\"info_update_checking_new_version\">புதிய பதிப்பைச் சரிபார்க்கிறது…</string>\n    <string name=\"info_image_saved_in_gallery\">கேலரியில் வெற்றிகரமாக சேமிக்கப்பட்டது.</string>\n    <string name=\"info_update_new_version_available\">புதிய பதிப்பு கிடைக்கிறது.</string>\n    <string name=\"info_update_new_version_available_text\">கிட்சூனின் பதிப்பு %s கிடைக்கின்றன.</string>\n    <string name=\"info_update_no_new_version_available\">புதிய பதிப்பு எதுவும் கிடைக்கவில்லை.</string>\n    <string name=\"info_update_failed\">புதிய பதிப்பைச் சரிபார்க்கத் தவறிவிட்டது.</string>\n    <string name=\"onboarding_welcome_title\">கிட்சூனுக்கு வருக!</string>\n    <string name=\"onboarding_welcome_subtitle\">உங்கள் தனிப்பட்ட அனிம் மற்றும் மங்கா துணை.</string>\n    <string name=\"onboarding_welcome_feature_search\">தேடல்</string>\n    <string name=\"onboarding_welcome_feature_search_description\">உங்களுக்கு பிடித்த அனிம் மற்றும் மங்காவைக் கண்டறியவும்.</string>\n    <string name=\"onboarding_welcome_feature_explore\">ஆராயுங்கள்</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">புதிய மற்றும் பிரபலமான தலைப்புகளைக் கண்டறியவும்.</string>\n    <string name=\"onboarding_welcome_feature_track\">மின்தடம்</string>\n    <string name=\"onboarding_welcome_feature_track_description\">நீங்கள் பார்த்த மற்றும் படித்தவற்றைக் கண்காணிக்கவும்.</string>\n    <string name=\"onboarding_welcome_action\">தொடங்கவும்</string>\n    <string name=\"onboarding_login_title\">உறுப்பினர் மற்றும் கோழி</string>\n    <string name=\"onboarding_login_subtitle\">உங்கள் கிட்சு கணக்கில் உள்நுழைக அல்லது அனைத்து அம்சங்களையும் அணுக புதிய கணக்கை உருவாக்கவும்.</string>\n    <string name=\"onboarding_login_forward_dialog_title\">கணக்கை உருவாக்கவும்</string>\n    <string name=\"preference_app_version\">பதிப்பு செய்தி</string>\n    <string name=\"dialog_remove_from_library_msg\">&lt;b&gt;%s&lt;/b&gt; உங்கள் நூலகத்திலிருந்து அகற்றப்படும்.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">இசைவு நிராகரிக்கப்பட்டது</string>\n    <string name=\"dialog_notification_permission_rejected\">உங்களுக்கு அறிவிப்புகள் கிடைக்காது, பயன்பாட்டு புதுப்பிப்புகள் குறித்து தெரிவிக்கப்படாது.</string>\n    <string name=\"dialog_request_notification_permission_title\">அறிவிப்புகளை அனுமதிக்கவும்</string>\n    <string name=\"dialog_request_notification_permission\">புதிய பயன்பாட்டு பதிப்பு வெளியிடப்படும் போது அறிவிக்கப்படுவதற்கு புச் அறிவிப்புகளை அனுமதிக்கவும்.</string>\n    <string name=\"error_resource_loading\">தரவை ஏற்ற முடியவில்லை.</string>\n    <string name=\"error_image_loading\">படத்தை ஏற்றுவதில் தோல்வி.</string>\n    <string name=\"error_mapping_loading\">வெளிப்புற தளங்களுக்கான மேப்பிங்சை ஏற்றுவதில் தோல்வி.</string>\n    <string name=\"error_nothing_found\">எதுவும் கிடைக்கவில்லை.</string>\n    <string name=\"error_invalid_user\">தவறான பயனர். நீங்கள் உள்நுழைந்திருக்கிறீர்களா?</string>\n    <string name=\"error_user_update_failed\">பயனர் தரவைப் புதுப்பிப்பதில் தோல்வி.</string>\n    <string name=\"error_user_delete_waifu_failed\">வைஃபு/கணவனை நீக்குவதில் தோல்வி.</string>\n    <string name=\"error_user_update_image_failed\">சுயவிவரப் படம் அல்லது அட்டையைப் புதுப்பிப்பதில் தோல்வி.</string>\n    <string name=\"error_user_create_profile_link_failed\">%s க்கான சுயவிவர இணைப்பை உருவாக்குவதில் தோல்வி.</string>\n    <string name=\"error_user_update_profile_link_failed\">%s க்கான சுயவிவர இணைப்பைப் புதுப்பிக்கத் தவறிவிட்டது.</string>\n    <string name=\"error_user_delete_profile_link_failed\">%s க்கான சுயவிவர இணைப்பை நீக்குவதில் தோல்வி.</string>\n    <string name=\"error_something_wrong\">அச்சச்சோ, ஏதோ தவறு நடந்தது!</string>\n    <string name=\"error_requires_external_storage_permission\">வெளிப்புற சேமிப்பகத்திற்கு எழுத இசைவு தேவை.</string>\n    <string name=\"error_search_provider_unavailable\">தேடல் வழங்குநர் கிடைக்கவில்லை.</string>\n    <string name=\"error_library_update_failed\">நூலக நுழைவைப் புதுப்பிக்கத் தவறிவிட்டது.</string>\n    <string name=\"error_library_update_failed_multiple\">%d நூலக உள்ளீடுகளைப் புதுப்பிக்கத் தவறிவிட்டது.</string>\n    <string name=\"error_library_update_not_found\">நூலக நுழைவு இல்லை.</string>\n    <string name=\"error_library_add_failed\">உங்கள் நூலகத்தில் சேர்க்கத் தவறிவிட்டது.</string>\n    <string name=\"error_library_delete_failed\">நூலக நுழைவை நீக்குவதில் தோல்வி.</string>\n    <string name=\"error_requires_notification_permission\">அறிவிப்பு இசைவு தேவை.</string>\n    <string name=\"sort_popularity_asc\">குறைந்தது பிரபலமானது</string>\n    <string name=\"sort_popularity_desc\">மிகவும் பிரபலமானது</string>\n    <string name=\"sort_average_rating_asc\">மோசமான மதிப்பிடப்பட்ட</string>\n    <string name=\"preference_start_fragment_description\">இயல்புநிலை பக்கம் %s என அமைக்கப்பட்டுள்ளது.</string>\n    <string name=\"preference_language\">மொழி</string>\n    <string name=\"preference_language_default\">இயல்புநிலை மொழி</string>\n    <string name=\"preference_display_name\">காட்சி பெயர்</string>\n    <string name=\"preference_profile_url\">சுயவிவர முகவரி</string>\n    <string name=\"preference_profile_url_not_set\">சுயவிவர முகவரி தொகுப்பு இல்லை.</string>\n    <string name=\"preference_country\">நாடு</string>\n    <string name=\"preference_country_summary\">நாடு %s ஆக அமைக்கப்பட்டுள்ளது</string>\n    <string name=\"preference_country_summary_non\">எந்த நாடும் குறிப்பிடப்படவில்லை.</string>\n    <string name=\"preference_titles_description\">ஊடக தலைப்புகள் எவ்வாறு காண்பிக்கப்படும் என்பதை வரையறுக்கவும்.</string>\n    <string name=\"preference_titles_canonical\">நியமன</string>\n    <string name=\"preference_titles_romanized\">ரோமானியச்</string>\n    <string name=\"preference_titles_english\">ஆங்கிலம்</string>\n    <string name=\"preference_adult_content\">வயதுவந்தோர் உள்ளடக்கம்</string>\n    <string name=\"preference_adult_content_description_sfw\">R18+ அகவை மதிப்பீட்டைக் கொண்ட ஊடகங்கள் மறைக்கப்படும்.</string>\n    <string name=\"preference_adult_content_description_sometimes\">R18+ அகவை மதிப்பீட்டைக் கொண்ட ஊடகங்கள் தெரியும், ஆனால் குறிக்கப்பட்ட பதிவுகள் NSFW உலகளாவிய ஊட்டத்தில் மறைக்கப்படும்.</string>\n    <string name=\"preference_adult_content_description_everywhere\">R18+ அகவை மதிப்பீட்டைக் கொண்ட ஊடகங்கள் எல்லா இடங்களிலும் தெரியும்.</string>\n    <string name=\"preference_sfw_filter_hide_all\">எல்லா வயதுவந்த உள்ளடக்கத்தையும் மறைக்கவும்</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">பின்வரும் ஊட்டத்திற்கு வரம்பு</string>\n    <string name=\"preference_sfw_filter_show_all\">எல்லா இடங்களிலும் அகவை வந்தோர் உள்ளடக்கம்</string>\n    <string name=\"preference_rating_system\">மதிப்பீட்டு அமைப்பு</string>\n    <string name=\"preference_rating_system_simple\">எளிய</string>\n    <string name=\"preference_rating_system_regular\">வழக்கமான</string>\n    <string name=\"preference_rating_system_advanced\">மேம்பட்ட</string>\n    <string name=\"preference_remember_search_filters\">தேடல் வடிப்பான்களை நினைவில் கொள்க</string>\n    <string name=\"preference_force_legacy_image_picker\">படை மரபு புகைப்படத் தேர்வாளர்</string>\n    <string name=\"preference_force_legacy_image_picker_description\">புதிய புகைப்பட தேர்வுக்கு பதிலாக கணினி கோப்பு தேர்வாளரைப் பயன்படுத்தவும்.</string>\n    <string name=\"preference_check_for_updates\">துவக்கத்தில் புதுப்பிப்புகளைச் சரிபார்க்கவும்</string>\n    <string name=\"preference_check_for_updates_description\">ஒவ்வொரு துவக்கத்திலும் புதிய பயன்பாட்டு பதிப்பைச் சரிபார்க்கவும்.</string>\n    <string name=\"preference_app_logs_description\">பயன்பாட்டு பதிவு செய்திகளைப் பார்த்து பகிரவும்.</string>\n    <string name=\"preference_app_version_description\">புதுப்பிப்புகளை சரிபார்க்க சொடுக்கு செய்க.</string>\n    <string name=\"preference_github_repo\">அறிவிலிமையம் களஞ்சியம்</string>\n    <string name=\"preference_open_source_libraries_description\">பயன்படுத்தப்பட்ட அனைத்து திறந்த மூல நூலகங்களின் பட்டியல்.</string>\n    <string name=\"preference_not_logged_in\">இந்த அமைப்பைத் திருத்த நீங்கள் உள்நுழைய வேண்டும்.</string>\n    <string name=\"app_logs_no_data\">பதிவுகள் எதுவும் கிடைக்கவில்லை.</string>\n    <string name=\"prompt_email\">மின்னஞ்சல்</string>\n    <string name=\"prompt_password\">கடவுச்சொல்</string>\n    <string name=\"action_sign_in\">விடுபதிகை</string>\n    <string name=\"action_log_in\">புகுபதிகை</string>\n    <string name=\"action_log_in_to_kitsu\">உறுப்பினர் மற்றும் கோழி</string>\n    <string name=\"action_create_account\">கணக்கை உருவாக்கவும்</string>\n    <string name=\"invalid_username\">சரியான மின்னஞ்சல் முகவரி அல்ல</string>\n    <string name=\"login_failed\">உள்நுழைவு தோல்வியடைந்தது</string>\n    <string name=\"status_logging_in\">உள்நுழைவு…</string>\n    <string name=\"logged_in_success\">%s ஆக உள்நுழைந்துள்ளது.</string>\n    <string name=\"login_no_account\">கணக்கு இல்லையா? <a href=\"https://kitsu.app\"> kitsu.app </a> இல் ஒன்றை உருவாக்கவும்.</string>\n    <string name=\"not_logged_in\">உள்நுழையவில்லை</string>\n    <string name=\"library_not_started\">தொடங்கப்படவில்லை</string>\n    <string name=\"library_not_rated\">மதிப்பிடப்படவில்லை</string>\n    <string name=\"library_not_logged_in_title\">உங்கள் நூலகத்தை அணுக உள்நுழைக</string>\n    <string name=\"library_not_logged_in_text\">உங்கள் நூலகத்தை நிர்வகிக்க மற்றும் அனைத்து அம்சங்களையும் அணுக உங்கள் கிட்சு கணக்கில் உள்நுழைக.</string>\n    <string name=\"library_kind_all\">அனைத்தும்</string>\n    <string name=\"library_not_synchronized\">ஒத்திசைக்கப்படவில்லை</string>\n    <string name=\"library_edit_status\">நூலக நிலை</string>\n    <string name=\"library_edit_progress\">முன்னேற்றம்</string>\n    <string name=\"library_edit_volumes\">தொகுதிகள்</string>\n    <string name=\"library_edit_rating\">செயல்வரம்பு</string>\n    <string name=\"library_edit_rewatch_count\">எண்ணிக்கையை மறுபரிசீலனை செய்யுங்கள்</string>\n    <string name=\"library_edit_reread_count\">எண்ணிக்கையை மீண்டும் படிக்கவும்</string>\n    <string name=\"library_edit_start_rewatch\">மறுபரிசீலனை செய்யத் தொடங்குங்கள்</string>\n    <string name=\"library_edit_start_reread\">மீண்டும் படிக்கத் தொடங்குங்கள்</string>\n    <string name=\"library_edit_privacy\">தனியுரிமை</string>\n    <string name=\"library_edit_privacy_public\">பொது</string>\n    <string name=\"library_edit_privacy_private\">தனிப்பட்ட</string>\n    <string name=\"library_edit_started\">தொடங்கியது</string>\n    <string name=\"info_logged_out_token_expired\">உங்கள் அணுகல் கிள்ளாக்கு காலாவதியானது. மீண்டும் உள்நுழைக.</string>\n    <string name=\"filter_kind\">தயவுசெய்து</string>\n    <string name=\"filter_year\">ஆண்டு</string>\n    <string name=\"filter_avg_rating\">சராசரி மதிப்பீடு</string>\n    <string name=\"filter_select_categories\">வகைகளைத் தேர்ந்தெடுக்கவும்</string>\n    <string name=\"filter_season\">சீசன்</string>\n    <string name=\"filter_subtype\">துணை வகை</string>\n    <string name=\"filter_streamers\">ச்ட்ரீமர்கள்</string>\n    <string name=\"filter_age_rating\">அகவை மதிப்பீடு</string>\n    <string name=\"profile_not_logged_in_title\">இங்கே பார்க்க எதுவும் இல்லை!</string>\n    <string name=\"profile_not_logged_in_text\">உங்கள் கிட்சு கணக்கில் உள் நுழைக அல்லது அனைத்து அம்சங்களையும் அணுக புதிய கணக்கை உருவாக்கவும்.</string>\n    <string name=\"profile_anime_stats\">அனிம் புள்ளிவிவரங்கள்</string>\n    <string name=\"profile_manga_stats\">மங்கா புள்ளிவிவரங்கள்</string>\n    <string name=\"profile_stats_anime_watch_time\">%s அனிமேசைப் பார்த்து செலவிட்டன.</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d அத்தியாயங்கள் படிக்கின்றன.</string>\n    <string name=\"profile_stats_completed\">%1$d முடிந்தது. &lt;b&gt;%2$d%%&lt;/b&gt; பயனர்களின்.</string>\n    <string name=\"profile_data_gender\">பாலினம்</string>\n    <string name=\"profile_data_location\">இடம்</string>\n    <string name=\"profile_data_birthday\">பிறந்த நாள்</string>\n    <string name=\"profile_data_join_date\">தேதியில் சேரவும்</string>\n    <string name=\"profile_data_join_date_ago\">%s</string>\n    <string name=\"profile_data_private\">இது ஒரு மறைபொருள்.</string>\n    <string name=\"profile_gender_male\">ஆண்</string>\n    <string name=\"profile_gender_female\">பெண்</string>\n    <string name=\"profile_gender_custom\">தனிப்பயன்</string>\n    <string name=\"profile_gender_custom_hint\">உங்கள் பாலினத்தை வரையறுக்கவும்</string>\n    <string name=\"profile_data_waifu\">வைஃபு</string>\n    <string name=\"profile_data_husbando\">கணவன்</string>\n    <string name=\"profile_waifu_or_husbando\">வைஃபு மற்றும் கணவர்</string>\n    <string name=\"profile_find_waifu\">உங்கள் சிறப்பு ஒருவரைக் கண்டுபிடி</string>\n    <string name=\"profile_data_bio\">உயிர்</string>\n    <string name=\"profile_cover_image_description\">கவர் படம்</string>\n    <string name=\"profile_avatar_image_description\">சுயவிவர படம்</string>\n    <string name=\"profile_link_url\">முகவரி அல்லது பயனர்பெயர்</string>\n    <string name=\"notification_updates\">பயன்பாட்டு புதுப்பிப்புகள்</string>\n    <string name=\"widget_description\">நூலக முன்னேற்றம்</string>\n    <string name=\"widget_empty_text\">உங்கள் நூலகத்தில் செயலில் கண்காணிக்கப்பட்ட உருப்படி எதுவும் இல்லை</string>\n    <string name=\"onboarding_login_forward_dialog_message\">நீங்கள் %1$s க்கு திருப்பி விடப்படுவீர்கள், அங்கு நீங்கள் ஒரு புதிய கணக்கை உருவாக்கலாம்.</string>\n    <string name=\"onboarding_setup_title\">தொடக்க அமைப்பு</string>\n    <string name=\"onboarding_setup_subtitle\">தொடங்க உங்களுக்கு விருப்பமான அமைப்புகளை அமைக்கவும். அமைப்புகளில் எப்போது வேண்டுமானாலும் அவற்றை மாற்றலாம்.</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">அறிவிப்பு இசைவு தேவை.</string>\n    <string name=\"onboarding_setup_updates\">புதுப்பிப்புகளை சரிபார்க்கவும்</string>\n    <string name=\"onboarding_setup_updates_description\">கிட்அப்பில் புதிய வெளியீடு கிடைக்கும்போது அறிவிக்கப்படும்.</string>\n    <string name=\"onboarding_setup_title_language\">தலைப்பு மொழி</string>\n    <string name=\"onboarding_setup_title_language_description\">தலைப்புகளுக்கு உங்களுக்கு விருப்பமான மொழியைத் தேர்ந்தெடுக்கவும்.</string>\n    <string name=\"onboarding_setup_title_language_selected\">தேர்ந்தெடுக்கப்பட்டது: %1$s</string>\n    <string name=\"onboarding_setup_action\">அமைவு</string>\n    <string name=\"unit_episode\">அத்தியாயம் %d</string>\n    <string name=\"unit_chapter\">அத்தியாயம் %d</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s பக்கம்</item>\n        <item quantity=\"other\">%s பக்கங்கள்</item>\n    </plurals>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s இரண்டாவது</item>\n        <item quantity=\"other\">%s வினாடிகள்</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-tr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Navigation Titles -->\n    <string name=\"nav_home\">Ana Sayfa</string>\n    <string name=\"nav_search\">Ara</string>\n    <string name=\"nav_library\">Kütüphane</string>\n    <string name=\"nav_profile\">Profil</string>\n    <string name=\"nav_settings\">Ayarlar</string>\n    <string name=\"nav_appearance\">Görünüş</string>\n    <string name=\"nav_app_logs\">Uygulama Günlüğü</string>\n    <string name=\"nav_os_libraries\">Açık Kaynak Kütüphaneleri</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"manga\">Manga</string>\n    <string name=\"action_retry\">Yenile</string>\n    <string name=\"action_ok\">Tamam</string>\n    <string name=\"action_cancel\">Durdur</string>\n    <string name=\"action_reset_filter\">Filtreyi sıfırla</string>\n    <string name=\"action_unselect_all\">Seçimi kaldır</string>\n    <string name=\"action_read_more\">Daha fazla bilgi</string>\n    <string name=\"action_read_less\">Daha az bilgi</string>\n    <string name=\"action_show_more\">Daha fazla göster</string>\n    <string name=\"action_show_less\">Daha az göster</string>\n    <string name=\"action_view\">İncele</string>\n    <string name=\"action_back\">Geri</string>\n    <string name=\"action_close\">Kapat</string>\n    <string name=\"action_log_out\">Çıkış</string>\n    <string name=\"action_share_profile_url\">Profili paylaş</string>\n    <string name=\"action_share\">Paylaş</string>\n    <string name=\"action_open_external\">Başka sekmede aç</string>\n    <string name=\"action_open_external_short\">Harici siteler</string>\n    <string name=\"action_share_app_logs\">Kayıtları paylaş</string>\n    <string name=\"action_add_to_favorites\">Favorilere ekle</string>\n    <string name=\"action_remove_from_favorites\">Favorilerden kaldır</string>\n    <string name=\"action_dismiss\">Reddet</string>\n    <string name=\"action_synchronize\">Sekronize</string>\n    <string name=\"action_save_in_gallery\">Galeriye Kaydet</string>\n    <string name=\"action_open_in_browser\">Tarayıcıda Aç</string>\n    <string name=\"action_open_on_mal\">MyAnimeList.net de Aç</string>\n    <string name=\"action_save_changes\">Değişikliği Kaydet</string>\n    <string name=\"action_edit_profile\">Profili Düzenle</string>\n    <string name=\"action_update_profile\">Profili Güncelle</string>\n    <string name=\"action_add\">Ekle</string>\n    <string name=\"action_remove\">Kaldır</string>\n    <string name=\"action_update\">Güncelle</string>\n    <string name=\"action_start\">Başlat</string>\n    <string name=\"action_allow\">İzin</string>\n    <string name=\"action_rate\">Puan</string>\n    <string name=\"action_update_rating\">Puanı Güncelle</string>\n    <string name=\"action_remove_rating\">Puanı Kaldır</string>\n    <string name=\"action_add_profile_link\">Profile Link Ekle</string>\n    <string name=\"action_edit_profile_link\">Profil Linkini Düzenle</string>\n    <string name=\"action_select_profile_link_site\">Siteyi Seç</string>\n    <string name=\"action_delete_profile_link\">Profil Linkini Sil</string>\n    <string name=\"hint_search\">Ara</string>\n    <string name=\"hint_search_characters\">Karakteri ara</string>\n    <string name=\"hint_mark_watched\">İzlenildi olarak işaretle.</string>\n    <string name=\"hint_mark_not_watched\">İzlenilmedi olarak işaretle.</string>\n    <string name=\"hint_rating\">Puanla</string>\n    <string name=\"dialog_log_out_confirmation\">Çıkış yapmak istediğine emin misin?</string>\n    <string name=\"dialog_remove_from_library_title\">Kütüphaneden kaldır?</string>\n    <string name=\"dialog_remove_from_library_msg\">&lt;b&gt;%s&lt;/b&gt; kütüphaneden kaldırılacak.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">İzin verilmedi</string>\n    <string name=\"dialog_notification_permission_rejected\">Bildirim almayacak ve uygulama güncellemeleri hakkında bilgilendirilmeyeceksiniz.</string>\n    <string name=\"dialog_request_notification_permission_title\">Bildirimlere izin ver</string>\n    <string name=\"dialog_request_notification_permission\">Yeni bir uygulama sürümü yayınlandığında bildirim almak için bildirimlere izin ver.</string>\n    <string name=\"error_resource_loading\">Veri yüklenemedi.</string>\n    <string name=\"error_image_loading\">Görüntü yüklenemedi.</string>\n    <string name=\"error_mapping_loading\">Harici siteler için eşleştirmeler yüklenemedi.</string>\n    <string name=\"error_nothing_found\">Bulunamadı.</string>\n    <string name=\"error_invalid_user\">Bilinmeyen kullanıcı. Sen mi giriş yaptın?</string>\n    <string name=\"error_user_update_failed\">Kullanıcı verisi güncellenemedi.</string>\n    <string name=\"error_user_delete_waifu_failed\">Silinemedi Waifu/Husbando.</string>\n    <string name=\"error_user_update_image_failed\">Profil resmi veya kapağı güncellenemedi.</string>\n    <string name=\"error_user_create_profile_link_failed\">Profil bağlantısı oluşturulamadı %s.</string>\n    <string name=\"error_user_update_profile_link_failed\">Profil bağlantısı güncellenemedi %s.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Profil bağlantısı silinemedi %s.</string>\n    <string name=\"error_something_wrong\">Hay aksi, bir şeyler ters gitti!</string>\n    <string name=\"error_requires_external_storage_permission\">Harici depolama alanına yazma izni gerekiyor.</string>\n    <string name=\"error_search_provider_unavailable\">Arama sağlayıcısı bulunamadı.</string>\n    <string name=\"error_library_update_failed\">Kütüphane güncellemesi başarısız.</string>\n    <string name=\"error_library_update_failed_multiple\">Güncellenemedi %d kütüphane girişleri.</string>\n    <string name=\"error_library_update_not_found\">Kütüphane kaydı mevcut değil.</string>\n    <string name=\"error_library_add_failed\">Küthaneye eklenemedi.</string>\n    <string name=\"error_library_delete_failed\">Kütüphane kaydı silinemedi.</string>\n    <string name=\"error_requires_notification_permission\">Bildirim izni gerekli.</string>\n    <string name=\"sort_popularity_asc\">En Az Popüler</string>\n    <string name=\"sort_popularity_desc\">En Popüler</string>\n    <string name=\"sort_average_rating_asc\">En Kötü Puanlı</string>\n    <string name=\"sort_average_rating_desc\">En İyi Puanlı</string>\n    <string name=\"sort_release_date_asc\">Eskiler</string>\n    <string name=\"sort_release_date_desc\">Yeniler</string>\n    <string name=\"title_media_type\">Medya Tipi</string>\n    <string name=\"title_character_appearances\">Karakter Görünümleri</string>\n    <string name=\"title_categories\">Kategoriler</string>\n    <string name=\"title_filter\">Filtre</string>\n    <string name=\"title_description\">Açıklama</string>\n    <string name=\"title_details\">Detaylar</string>\n    <string name=\"title_streamer\">İzleme Bağlantıları</string>\n    <string name=\"title_ratings\">Puanlar</string>\n    <string name=\"title_more_franchise\">Bu seriden daha fazla</string>\n    <string name=\"title_favorite_anime\">Favori Anime</string>\n    <string name=\"title_favorite_manga\">Favori Manga</string>\n    <string name=\"title_favorite_characters\">Favori Karakterler</string>\n    <string name=\"title_authentication\">Doğrulama</string>\n    <string name=\"title_login\">Kitsu hesabına giriş yap</string>\n    <string name=\"title_play_trailer\">Fragmanı Oynat</string>\n    <string name=\"title_about_me\">Hakkımda</string>\n    <string name=\"title_episodes\">Sezonlar</string>\n    <string name=\"title_chapters\">Bölümler</string>\n    <string name=\"title_characters\">Karakterler</string>\n    <string name=\"title_edit_library_entry\">Kütüphane Girişini Güncelle</string>\n    <string name=\"title_edit_profile\">Profili Düzenle</string>\n    <string name=\"no_information\">Bilinmeyen</string>\n    <string name=\"no_data_available\">Veri Bulunamadı</string>\n    <string name=\"status_current\">Şu An Yayında</string>\n    <string name=\"status_current_manga\">Yayıncı</string>\n    <string name=\"status_finished\">Bitiş</string>\n    <string name=\"status_tba\">TBA</string>\n    <string name=\"status_unreleased\">Yayınlanmamış</string>\n    <string name=\"status_upcoming\">Yakında</string>\n    <string name=\"character_role_main\">Ana</string>\n    <string name=\"character_role_supporting\">Destekle</string>\n    <string name=\"character_role_recurring\">Tekrarlanan</string>\n    <string name=\"character_role_cameo\">Cameo</string>\n    <string name=\"library_status_watching\">İzle</string>\n    <string name=\"library_status_planned\">İzlemek İstediklerim</string>\n    <string name=\"library_status_completed\">Tamamlananlar</string>\n    <string name=\"library_status_on_hold\">Beklemede</string>\n    <string name=\"library_status_dropped\">Çıkar</string>\n    <string name=\"library_status_reading\">Okunan</string>\n    <string name=\"library_status_planned_manga\">Okumak İstediklerim</string>\n    <string name=\"library_action_remove\">Kütüphaneden Kaldır</string>\n    <string name=\"library_action_add\">Kütüphaneye Ekle</string>\n    <string name=\"library_action_edit\">Kütüphaneyi Düzenle</string>\n    <string name=\"data_rank\">Seviye #%d</string>\n    <string name=\"data_popularity\">Popülerite #%d</string>\n    <string name=\"data_kitsu_score\">Kitsu Skoru %s%%</string>\n    <string name=\"data_length_total\">%s toplam</string>\n    <string name=\"data_length_each\">%d geçen dakika</string>\n    <string name=\"data_title_english\">İngilizce</string>\n    <string name=\"data_title_japanese\">Japonca</string>\n    <string name=\"data_title_japanese_romaji\">Japonca (Romaji)</string>\n    <string name=\"data_abbreviated_titles\">Eş Anlamlılar</string>\n    <string name=\"data_other_names\">Ayrıca şöyle bilinir</string>\n    <string name=\"data_title_type\">Tipi</string>\n    <string name=\"data_title_status\">Durumu</string>\n    <string name=\"data_title_aired\">Yayınlanan</string>\n    <string name=\"data_title_published\">Yayınlandı</string>\n    <string name=\"data_title_tba\">Beklenen</string>\n    <string name=\"data_title_season\">Sezon</string>\n    <string name=\"data_title_age_rating\">Puan</string>\n    <string name=\"data_title_serialization\">Serileştirme</string>\n    <string name=\"data_title_chapters\">Bölümler</string>\n    <string name=\"data_title_volumes\">Ciltler</string>\n    <string name=\"data_title_episodes\">Sezonlar</string>\n    <string name=\"data_title_length\">Uzunluk</string>\n    <string name=\"data_title_producers\">Yapımcıları</string>\n    <string name=\"data_title_licensors\">Lisans Sahipleri</string>\n    <string name=\"data_title_studios\">Stüdyolar</string>\n    <string name=\"season_winter\">Kış</string>\n    <string name=\"season_spring\">İlkbahar</string>\n    <string name=\"season_summer\">Yaz</string>\n    <string name=\"season_fall\">Sonbahar</string>\n    <string name=\"relationship_sequel\">Devamı</string>\n    <string name=\"relationship_prequel\">Öncesi</string>\n    <string name=\"relationship_alternative_setting\">Alt. ayarlar</string>\n    <string name=\"relationship_alternative_version\">Alt. versiyon</string>\n    <string name=\"relationship_side_story\">Yan hikaye</string>\n    <string name=\"relationship_parent_story\">Ebeveyn hikayesi</string>\n    <string name=\"relationship_summary\">Özet</string>\n    <string name=\"relationship_full_story\">Bütün hikaye</string>\n    <string name=\"relationship_spinoff\">Yan ürün</string>\n    <string name=\"relationship_adaptation\">Adaptasyon</string>\n    <string name=\"relationship_character\">Karakter</string>\n    <string name=\"relationship_other\">Diğer</string>\n    <string name=\"section_trending\">Haftanın Trendleri</string>\n    <string name=\"section_top_airing_anime\">En Çok Gösterilen Anime</string>\n    <string name=\"section_top_upcoming_anime\">Gelecek En İyi Anime</string>\n    <string name=\"section_highest_rated_anime\">En Yüksek Puanlı Anime</string>\n    <string name=\"section_most_popular_anime\">En Popüler Anime</string>\n    <string name=\"section_top_airing_manga\">En İyi Manga Yayınları</string>\n    <string name=\"section_top_upcoming_manga\">Gelecek En İyi Manga</string>\n    <string name=\"section_highest_rated_manga\">En Yüksek Puanlı Manga</string>\n    <string name=\"section_most_popular_manga\">En Popüler Manga</string>\n    <string name=\"preference_category_ui\">Kullanıcı Arayüzü</string>\n    <string name=\"preference_category_account\">Hesap</string>\n    <string name=\"preference_category_advanced\">Gelişmiş</string>\n    <string name=\"preference_category_about\">Hakkında</string>\n    <string name=\"preference_appearance_description\">Temayı değiştirin ve karanlık modu açın.</string>\n    <string name=\"preference_dark_mode\">Karanlık Tema</string>\n    <string name=\"preference_dark_mode_light\">Aydınlık</string>\n    <string name=\"preference_dark_mode_dark\">Karanlık</string>\n    <string name=\"preference_dark_mode_follow_system\">Sistem Temasına Uy</string>\n    <string name=\"preference_dark_mode_battery_saver\">Pil Tasarrufu</string>\n    <string name=\"preference_oled_black_mode\">OLED Siyah</string>\n    <string name=\"preference_oled_black_mode_description\">Karanlık modda siyah arka plan kullan.</string>\n    <string name=\"preference_app_theme\">Tema</string>\n    <string name=\"preference_dynamic_color_theme\">Dinamik Renk Seç</string>\n    <string name=\"preference_dynamic_color_theme_description\">Sistem temasına uy.</string>\n    <string name=\"preference_app_theme_default\">Varsayılan</string>\n    <string name=\"preference_app_theme_purple\">Mor</string>\n    <string name=\"preference_app_theme_blue\">Mavi</string>\n    <string name=\"preference_app_theme_green\">Yeşil</string>\n    <string name=\"preference_media_item_size\">Poster Görüntü Boyutu</string>\n    <string name=\"preference_media_item_size_small\">Küçük</string>\n    <string name=\"preference_media_item_size_medium\">Orta</string>\n    <string name=\"preference_media_item_size_large\">Büyük</string>\n    <string name=\"preference_start_fragment\">Varsayılan Sayfa</string>\n    <string name=\"preference_start_fragment_home\">Ev</string>\n    <string name=\"preference_start_fragment_search\">Ara</string>\n    <string name=\"preference_start_fragment_library\">Kütüphane</string>\n    <string name=\"preference_start_fragment_profile\">Profil</string>\n    <string name=\"preference_start_fragment_description\">Varsayılan sayfa olarak ayarlandı %s.</string>\n    <string name=\"preference_language\">Dil</string>\n    <string name=\"preference_display_name\">Görünen Ad</string>\n    <string name=\"preference_profile_url\">Profil URL</string>\n    <string name=\"preference_profile_url_not_set\">Profil URL\\'si ayarlanmadı.</string>\n    <string name=\"preference_country\">Ülke</string>\n    <string name=\"preference_country_summary\">Ülke şu şekilde ayarlandı %s</string>\n    <string name=\"preference_country_summary_non\">Ülke belirtilmedi.</string>\n    <string name=\"preference_titles\">Başlıklar</string>\n    <string name=\"preference_titles_description\">Medya başlıklarının nasıl görüntüleneceğini belirle.</string>\n    <string name=\"preference_titles_canonical\">Standart</string>\n    <string name=\"preference_titles_romanized\">Romanize</string>\n    <string name=\"preference_titles_english\">İngilizce</string>\n    <string name=\"preference_adult_content\">Yetişkin İçeriği</string>\n    <string name=\"preference_adult_content_description_sfw\">R18+ yaş derecelendirmesi olan medya gizlenecek.</string>\n    <string name=\"preference_adult_content_description_sometimes\">R18+ yaş derecelendirmesi olan medya görünür olacak, ancak NSFW etiketiyle işaretlenmiş gönderiler genel akışta gizlenecek.</string>\n    <string name=\"preference_adult_content_description_everywhere\">R18+ yaş derecelendirmesine sahip medya her yerde görünür olacak.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Tüm Yetişkin İçeriğini Gizle</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Aşağıdaki Akışla Sınırla</string>\n    <string name=\"preference_sfw_filter_show_all\">Her Yerde Yetişkin İçeriği</string>\n    <string name=\"preference_rating_system\">Puan Sistemi</string>\n    <string name=\"preference_rating_system_simple\">Basit</string>\n    <string name=\"preference_rating_system_regular\">Normal</string>\n    <string name=\"preference_rating_system_advanced\">Gelişmiş</string>\n    <string name=\"preference_remember_search_filters\">Arama Filtrelerini Hatırla</string>\n    <string name=\"preference_remember_search_filters_description\">Uygulama yeniden başlatıldıktan sonra son kullanılan arama filtrelerini uygula.</string>\n    <string name=\"preference_force_legacy_image_picker\">Geçmiş Fotoğraf Seçiciyi Zorla</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Sistem dosya seçicisini kullanmak için yeni fotoğraf seçicisi yerine zorla.</string>\n    <string name=\"preference_check_for_updates\">Başlangıçta Güncellemeleri Kontrol Et</string>\n    <string name=\"preference_check_for_updates_description\">Her başlatmada yeni bir uygulama sürümü olup olmadığını kontrol edin.</string>\n    <string name=\"preference_app_logs_description\">Uygulama günlük mesajlarını gör ve paylaş.</string>\n    <string name=\"preference_app_version\">Versiyon Bİlgisi</string>\n    <string name=\"preference_app_version_description\">Tıkla ve güncellemeyi kontrol et.</string>\n    <string name=\"preference_github_repo\">GitHub Deposu</string>\n    <string name=\"preference_open_source_libraries_description\">Kullanılan tüm açık kaynak kütüphanelerinin listesi.</string>\n    <string name=\"preference_not_logged_in\">Bu ayarı düzenlemek için oturum açmalısınız.</string>\n    <string name=\"app_logs_no_data\">Kayıt mevcut değil.</string>\n    <string name=\"prompt_email\">Email</string>\n    <string name=\"prompt_password\">Şifre</string>\n    <string name=\"action_sign_in\">Oturum aç</string>\n    <string name=\"action_log_in\">Giriş yap</string>\n    <string name=\"action_log_in_to_kitsu\">Kitsu ile giriş yap</string>\n    <string name=\"invalid_username\">Email adresi bulunamadı</string>\n    <string name=\"login_failed\">\"Giriş başarısız\"</string>\n    <string name=\"status_logging_in\">Giriş…</string>\n    <string name=\"logged_in_success\">Olarak oturum açtı %s.</string>\n    <string name=\"login_no_account\">Hesabınız yok mu? Bir tane oluşturun <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"not_logged_in\">Giriş yapılamadı</string>\n    <string name=\"library_not_started\">Başlatılamadı</string>\n    <string name=\"library_not_rated\">Derecelendirilmemiş</string>\n    <string name=\"library_not_logged_in_title\">Kütüphanenize erişmek için giriş yapın</string>\n    <string name=\"library_not_logged_in_text\">Kütüphanenizi yönetmek ve tüm özelliklere erişmek için Kitsu Hesabınıza giriş yapın.</string>\n    <string name=\"library_kind_all\">Hepsi</string>\n    <string name=\"library_not_synchronized\">Sekronize Değil</string>\n    <string name=\"library_edit_status\">Kütüphane Durumu</string>\n    <string name=\"library_edit_progress\">İlerleme</string>\n    <string name=\"library_edit_volumes\">Ciltler</string>\n    <string name=\"library_edit_rating\">Derecelendirme</string>\n    <string name=\"library_edit_rewatch_count\">Tekrar İzleme Sayısı</string>\n    <string name=\"library_edit_reread_count\">Tekrar Okuma Sayısı</string>\n    <string name=\"library_edit_start_rewatch\">Tekrar İzlemeye Başlat</string>\n    <string name=\"library_edit_start_reread\">Tekrar Okumaya Başlat</string>\n    <string name=\"library_edit_privacy\">Gizlilik</string>\n    <string name=\"library_edit_privacy_public\">Halka Açık</string>\n    <string name=\"library_edit_privacy_private\">Özel</string>\n    <string name=\"library_edit_started\">Başlatıldı</string>\n    <string name=\"library_edit_finished\">Bitirildi</string>\n    <string name=\"library_edit_no_date_set\">Tarih belirlenmemiş</string>\n    <string name=\"library_edit_notes\">Kişisel Notları</string>\n    <string name=\"info_log_in_required\">Tüm özelliklere erişmek için giriş yapın.</string>\n    <string name=\"info_image_saved_in_gallery\">Görsel galeriye başarıyla kaydedildi.</string>\n    <string name=\"info_update_checking_new_version\">Yeni sürüm kontrol ediliyor…</string>\n    <string name=\"info_update_new_version_available\">Yeni sürüm mevcut.</string>\n    <string name=\"info_update_new_version_available_text\">Sürüm %s Kitsune\\'un bir sürüm mevcut.</string>\n    <string name=\"info_update_no_new_version_available\">Yeni sürümü mevcut değil.</string>\n    <string name=\"info_update_failed\">Yeni sürüm kontrolü başarısız oldu.</string>\n    <string name=\"info_logged_out_token_expired\">Erişim tokeninizin süresi doldu. Lütfen tekrar giriş yapın.</string>\n    <string name=\"filter_kind\">Tür</string>\n    <string name=\"filter_year\">Yıl</string>\n    <string name=\"filter_avg_rating\">Ortalama Puan</string>\n    <string name=\"filter_select_categories\">Kategori Seç</string>\n    <string name=\"filter_season\">Sezon</string>\n    <string name=\"filter_subtype\">Alt Tür</string>\n    <string name=\"filter_streamers\">Yayıncılar</string>\n    <string name=\"filter_age_rating\">Yaş Derecelendirmesi</string>\n    <string name=\"profile_not_logged_in_title\">Burada görülecek bir şey yok!</string>\n    <string name=\"profile_not_logged_in_text\">Tüm özelliklere erişmek için Kitsu Hesabınıza giriş yapın veya yeni bir Hesap oluşturun.</string>\n    <string name=\"profile_anime_stats\">Anime İstatistikleri</string>\n    <string name=\"profile_manga_stats\">Manga İstatistikleri</string>\n    <string name=\"profile_stats_anime_watch_time\">%s anime izleyerek geçirdi.</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d bölümler okundu.</string>\n    <string name=\"profile_stats_time_spent_total\">%s toplamda.</string>\n    <string name=\"profile_stats_completed\">%1$d tamamlandı. Daha fazla &lt;b&gt;%2$d%%&lt;/b&gt; kullanıcıların.</string>\n    <string name=\"profile_data_gender\">Cinsiyet</string>\n    <string name=\"profile_data_location\">Konum</string>\n    <string name=\"profile_data_birthday\">Doğum Tarihi</string>\n    <string name=\"profile_data_join_date\">Katılım Tarihi</string>\n    <string name=\"profile_data_join_date_ago\">%s önce</string>\n    <string name=\"profile_data_private\">Bu bir sır.</string>\n    <string name=\"profile_gender_male\">Erkek</string>\n    <string name=\"profile_gender_female\">Kadın</string>\n    <string name=\"profile_gender_custom\">Özel</string>\n    <string name=\"profile_gender_custom_hint\">Cinsiyetinizi belirleyin</string>\n    <string name=\"profile_data_waifu\">Waifu</string>\n    <string name=\"profile_data_husbando\">Husbando</string>\n    <string name=\"profile_waifu_or_husbando\">Waifu veya Husbando</string>\n    <string name=\"profile_find_waifu\">Özel kişiyi bulun</string>\n    <string name=\"profile_data_bio\">Özgeçmiş</string>\n    <string name=\"profile_cover_image_description\">Kapak görseli</string>\n    <string name=\"profile_avatar_image_description\">Profil fotoğrafı</string>\n    <string name=\"profile_link_url\">URL veya Kullanıcı Adı</string>\n    <string name=\"notification_updates\">Uygulama Güncellemeleri</string>\n    <string name=\"unit_episode\">Sezon %d</string>\n    <string name=\"unit_chapter\">Bölüm %d</string>\n    <!-- Unit Plurals -->\n    <plurals name=\"unit_pages\">\n        <item quantity=\"one\">%s Sayfa</item>\n        <item quantity=\"other\">%s Sayfalar</item>\n    </plurals>\n    <!-- Duration Plurals -->\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"one\">%s saniye</item>\n        <item quantity=\"other\">%s saniyeler</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"one\">%s dakika</item>\n        <item quantity=\"other\">%s dakikalar</item>\n    </plurals>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"one\">%s saat</item>\n        <item quantity=\"other\">%s saatler</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"one\">%s gün</item>\n        <item quantity=\"other\">%s günler</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"one\">%s ay</item>\n        <item quantity=\"other\">%s aylar</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"one\">%s yıl</item>\n        <item quantity=\"other\">%s yıllar</item>\n    </plurals>\n    <string name=\"preference_language_default\">varsayılan dil</string>\n    <string name=\"widget_description\">Kütüphane ilerlemesi</string>\n    <string name=\"widget_empty_text\">Kütüphanenizde izlenen etkin bir öge yok</string>\n    <string name=\"widget_empty_action\">Kütüphaneyi Aç</string>\n    <string name=\"onboarding_setup_title_language_description\">Başlıklar için tercih ettiğiniz dili seçin.</string>\n    <string name=\"onboarding_welcome_title\">Kitsune\\'ye Hoşgeldin!</string>\n    <string name=\"onboarding_welcome_subtitle\">Kişisel anime ve manga dostun.</string>\n    <string name=\"onboarding_welcome_feature_search\">Ara</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Favori anime ve manganızı bulun.</string>\n    <string name=\"onboarding_welcome_feature_explore\">Keşfet</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Yeni ve trend olan başlıkları keşfet.</string>\n    <string name=\"onboarding_welcome_feature_track\">Takip et</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Neler izleyip okuduysan takibini yap.</string>\n    <string name=\"onboarding_welcome_action\">Başlarken</string>\n    <string name=\"onboarding_login_title\">Kitsu\\'ya giriş yap</string>\n    <string name=\"onboarding_login_is_logged_in\">Giriş yaptın</string>\n    <string name=\"onboarding_login_subtitle\">Tüm özelliklere erişebilmek için Kitsu hesabına giriş yap veya yeni bir hesap oluştur.</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Hesap Oluştur</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Yeni bir hesap oluşturabileceğin %1$s\\'e yönlendirileceksin.</string>\n    <string name=\"onboarding_setup_title\">İlk kurulum</string>\n    <string name=\"onboarding_setup_subtitle\">Başlarken tercih ettiğiniz ayarları kaydedin. Bunları daha sonra ayarlardan istediğiniz zaman değiştirebilirsiniz.</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">Bildirim izni gerekli.</string>\n    <string name=\"onboarding_setup_updates\">Güncelleştirmeler İçin Kontrol Et</string>\n    <string name=\"onboarding_setup_updates_description\">GitHub\\'da yeni bir sürüm mevcut olduğunda haberdar ol.</string>\n    <string name=\"onboarding_setup_title_language_selected\">Seçildi: %1$s</string>\n    <string name=\"onboarding_setup_action\">Kurulumu Bitir</string>\n    <string name=\"action_skip\">Geç</string>\n    <string name=\"action_next\">İleri</string>\n    <string name=\"action_continue\">Devam Et</string>\n    <string name=\"action_create_account\">Hesap oluştur</string>\n    <string name=\"onboarding_setup_title_language\">Başlık Dili</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-v31/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"Glance.AppWidget.LinearProgressIndicator\" parent=\"@style/Widget.AppCompat.ProgressBar.Horizontal\">\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-vi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"sort_average_rating_asc\">Xếp hạng thấp nhất</string>\n    <string name=\"section_highest_rated_manga\">Manga được xếp hạng cao nhất</string>\n    <string name=\"library_not_rated\">Chưa xếp hạng</string>\n    <string name=\"error_user_update_image_failed\">Không cập nhật được ảnh hồ sơ hoặc ảnh bìa.</string>\n    <string name=\"error_user_create_profile_link_failed\">Không tạo được liên kết hồ sơ cho %s.</string>\n    <string name=\"title_edit_profile\">Chỉnh sửa hồ sơ</string>\n    <string name=\"preference_profile_url\">URL hồ sơ</string>\n    <string name=\"preference_profile_url_not_set\">Chưa đặt URL hồ sơ.</string>\n    <string name=\"profile_not_logged_in_text\">Đăng nhập vào tài khoản Kitsu của bạn hoặc tạo tài khoản mới để có quyền truy cập vào tất cả các tính năng.</string>\n    <string name=\"profile_anime_stats\">Thống kê Anime</string>\n    <string name=\"profile_manga_stats\">Thống kê Manga</string>\n    <string name=\"profile_stats_anime_watch_time\">%s đã dành để xem anime.</string>\n    <string name=\"profile_data_private\">Đó là bí mật.</string>\n    <string name=\"profile_data_gender\">Giới tính</string>\n    <string name=\"profile_data_location\">Vị trí</string>\n    <string name=\"profile_data_birthday\">Sinh nhật</string>\n    <string name=\"profile_gender_female\">Nữ giới</string>\n    <string name=\"profile_find_waifu\">Tìm người đặc biệt của bạn</string>\n    <string name=\"profile_data_bio\">Tiểu sử</string>\n    <string name=\"profile_cover_image_description\">Ảnh bìa</string>\n    <string name=\"profile_avatar_image_description\">Ảnh đại diện</string>\n    <string name=\"profile_link_url\">URL hoặc tên người dùng</string>\n    <string name=\"title_favorite_manga\">Manga ưa thích</string>\n    <string name=\"section_top_upcoming_manga\">Manga sắp ra mắt hàng đầu</string>\n    <string name=\"dialog_log_out_confirmation\">Bạn có chắc chắn bạn muốn đăng xuất?</string>\n    <string name=\"status_current_manga\">Xuất bản</string>\n    <string name=\"library_status_planned_manga\">Muốn đọc</string>\n    <string name=\"hint_rating\">Xếp hạng</string>\n    <string name=\"dialog_remove_from_library_msg\">&lt;b&gt;%s&lt;/b&gt; sẽ bị loại khỏi thư viện của bạn.</string>\n    <string name=\"dialog_notification_permission_rejected\">Bạn sẽ không nhận được thông báo và sẽ không được thông báo về các bản cập nhật ứng dụng.</string>\n    <string name=\"error_resource_loading\">Không thể tải dữ liệu.</string>\n    <string name=\"error_image_loading\">Không thể tải hình ảnh.</string>\n    <string name=\"error_something_wrong\">Rất tiếc, đã xảy ra lỗi!</string>\n    <string name=\"error_library_update_not_found\">Mục thư viện không tồn tại.</string>\n    <string name=\"sort_popularity_asc\">Ít phổ biến nhất</string>\n    <string name=\"sort_popularity_desc\">Phổ biến nhất</string>\n    <string name=\"sort_release_date_asc\">Cũ nhất</string>\n    <string name=\"title_media_type\">Loại phương tiện</string>\n    <string name=\"title_description\">Mô tả</string>\n    <string name=\"title_ratings\">Xếp hạng</string>\n    <string name=\"no_information\">Không xác định</string>\n    <string name=\"status_finished\">Hoàn thành</string>\n    <string name=\"status_unreleased\">Chưa phát hành</string>\n    <string name=\"character_role_supporting\">Hỗ trợ</string>\n    <string name=\"library_action_remove\">Loại khỏi thư viện</string>\n    <string name=\"action_add_to_favorites\">Thêm vào ưa thích</string>\n    <string name=\"nav_settings\">Thiết đặt</string>\n    <string name=\"nav_appearance\">Diện mạo</string>\n    <string name=\"manga\">Manga</string>\n    <string name=\"anime\">Anime</string>\n    <string name=\"action_ok\">OK</string>\n    <string name=\"action_view\">Hiện</string>\n    <string name=\"action_close\">Đóng</string>\n    <string name=\"action_share\">Chia sẻ</string>\n    <string name=\"action_open_external\">Mở trên một trang bên ngoài</string>\n    <string name=\"action_open_external_short\">Trang bên ngoài</string>\n    <string name=\"action_synchronize\">Đồng bộ hóa</string>\n    <string name=\"action_open_in_browser\">Mở trong trình duyệt</string>\n    <string name=\"action_open_on_mal\">Mở trên MyAnimeList.net</string>\n    <string name=\"action_update_profile\">Cập nhật hồ sơ</string>\n    <string name=\"action_add\">Thêm</string>\n    <string name=\"action_remove\">Loại bỏ</string>\n    <string name=\"action_dismiss\">Bỏ qua</string>\n    <string name=\"action_remove_from_favorites\">Loại bỏ ưa thích</string>\n    <string name=\"action_update_rating\">Cập nhật xếp hạng</string>\n    <string name=\"action_rate\">Xếp hạng</string>\n    <string name=\"action_remove_rating\">Loại bỏ xếp hạng</string>\n    <string name=\"action_add_profile_link\">Thêm liên kết hồ sơ</string>\n    <string name=\"hint_search\">Tìm kiếm</string>\n    <string name=\"hint_search_characters\">Tìm kiếm nhân vật</string>\n    <string name=\"hint_mark_watched\">Đánh dấu là đã xem.</string>\n    <string name=\"hint_mark_not_watched\">Đánh dấu là chưa xem.</string>\n    <string name=\"dialog_notification_permission_rejected_title\">Quyền bị từ chối</string>\n    <string name=\"dialog_request_notification_permission_title\">Cho phép thông báo</string>\n    <string name=\"dialog_request_notification_permission\">Cho phép thông báo đẩy để nhận thông báo khi phiên bản ứng dụng mới được phát hành.</string>\n    <string name=\"dialog_remove_from_library_title\">Loại khỏi thư viện?</string>\n    <string name=\"error_mapping_loading\">Không thể tải ánh xạ cho các trang ngoài.</string>\n    <string name=\"error_nothing_found\">Không tìm thấy.</string>\n    <string name=\"error_user_update_profile_link_failed\">Không cập nhật được liên kết hồ sơ cho %s.</string>\n    <string name=\"error_user_delete_profile_link_failed\">Không xóa được liên kết hồ sơ cho %s.</string>\n    <string name=\"error_invalid_user\">Người dùng không hợp lệ. Bạn đã đăng nhập chưa?</string>\n    <string name=\"error_user_update_failed\">Không thể cập nhật dữ liệu người dùng.</string>\n    <string name=\"error_user_delete_waifu_failed\">Không xóa được Waifu/Husbando.</string>\n    <string name=\"error_requires_external_storage_permission\">Yêu cầu quyền ghi vào bộ nhớ ngoài.</string>\n    <string name=\"error_library_update_failed\">Không thể cập nhật mục nhập thư viện.</string>\n    <string name=\"sort_average_rating_desc\">Xếp hạng tốt nhất</string>\n    <string name=\"error_library_update_failed_multiple\">Không thể cập nhật các mục nhập thư viện %d.</string>\n    <string name=\"error_library_add_failed\">Không thể thêm vào thư viện của bạn.</string>\n    <string name=\"error_library_delete_failed\">Không thể xóa mục thư viện.</string>\n    <string name=\"error_requires_notification_permission\">Cần được cấp quyền thông báo.</string>\n    <string name=\"sort_release_date_desc\">Mới nhất</string>\n    <string name=\"title_character_appearances\">Diện mạo nhân vật</string>\n    <string name=\"title_categories\">Danh mục</string>\n    <string name=\"title_filter\">Lọc</string>\n    <string name=\"title_details\">Chi tiết</string>\n    <string name=\"title_streamer\">Liên kết truyền phát</string>\n    <string name=\"title_more_franchise\">Thêm nữa từ bộ này</string>\n    <string name=\"title_episodes\">Tập</string>\n    <string name=\"title_favorite_anime\">Anime ưa thích</string>\n    <string name=\"title_favorite_characters\">Nhân vật ưa thích</string>\n    <string name=\"title_authentication\">Xác thực</string>\n    <string name=\"title_chapters\">Chương</string>\n    <string name=\"title_characters\">Nhân vật</string>\n    <string name=\"title_edit_library_entry\">Chỉnh sửa mục nhập thư viện</string>\n    <string name=\"no_data_available\">Không có dữ liệu</string>\n    <string name=\"status_current\">Đang phát sóng</string>\n    <string name=\"status_upcoming\">Sắp tới</string>\n    <string name=\"character_role_main\">Chính</string>\n    <string name=\"library_status_watching\">Đang xem</string>\n    <string name=\"library_status_planned\">Muốn xem</string>\n    <string name=\"library_status_completed\">Hoàn thành</string>\n    <string name=\"library_status_on_hold\">Đang chờ</string>\n    <string name=\"library_status_dropped\">Đã bỏ</string>\n    <string name=\"library_status_reading\">Đang đọc</string>\n    <string name=\"library_action_add\">Thêm vào thư viện</string>\n    <string name=\"library_action_edit\">Chỉnh sửa mục nhập thư viện</string>\n    <string name=\"data_rank\">Hạng #%d</string>\n    <string name=\"data_popularity\">Độ phổ biến #%d</string>\n    <string name=\"data_kitsu_score\">Điểm Kitsu %s%%</string>\n    <string name=\"data_title_english\">Tiếng Anh</string>\n    <string name=\"data_title_japanese_romaji\">Tiếng Nhật (Romaji)</string>\n    <string name=\"data_other_names\">Còn được biết là</string>\n    <string name=\"data_title_type\">Loại</string>\n    <string name=\"data_title_status\">Trạng thái</string>\n    <string name=\"data_title_published\">Đã phát hành</string>\n    <string name=\"data_title_season\">Mùa</string>\n    <string name=\"data_title_age_rating\">Xếp hạng</string>\n    <string name=\"data_length_each\">%d phút mỗi tập</string>\n    <string name=\"data_abbreviated_titles\">Tên khác</string>\n    <string name=\"data_title_volumes\">Cuốn</string>\n    <string name=\"data_title_length\">Độ dài</string>\n    <string name=\"data_title_studios\">Xưởng phim</string>\n    <string name=\"season_winter\">Mùa đông</string>\n    <string name=\"season_spring\">Mùa xuân</string>\n    <string name=\"season_fall\">Mùa thu</string>\n    <string name=\"relationship_sequel\">Phần tiếp theo</string>\n    <string name=\"relationship_prequel\">Phần tiền truyện</string>\n    <string name=\"relationship_parent_story\">Truyện gốc</string>\n    <string name=\"section_trending\">Xu hướng tuần này</string>\n    <string name=\"section_top_airing_anime\">Anime đang phát sóng hàng đầu</string>\n    <string name=\"section_highest_rated_anime\">Anime được xếp hạng cao nhất</string>\n    <string name=\"section_top_airing_manga\">Manga xuất bản hàng đầu</string>\n    <string name=\"section_most_popular_manga\">Manga phổ biến nhất</string>\n    <string name=\"section_most_popular_anime\">Anime phổ biến nhất</string>\n    <string name=\"preference_category_ui\">Giao diện người dùng</string>\n    <string name=\"preference_category_account\">Tài khoản</string>\n    <string name=\"preference_category_about\">Giới thiệu</string>\n    <string name=\"preference_dark_mode_light\">Sáng</string>\n    <string name=\"preference_dark_mode_dark\">Tối</string>\n    <string name=\"preference_dark_mode_follow_system\">Theo hệ thống</string>\n    <string name=\"preference_dark_mode_battery_saver\">Tiết kiệm pin</string>\n    <string name=\"preference_oled_black_mode\">OLED đen</string>\n    <string name=\"preference_dynamic_color_theme_description\">Thích ứng với chủ đề hệ thống.</string>\n    <string name=\"preference_app_theme_blue\">Màu lam</string>\n    <string name=\"preference_start_fragment_profile\">Hồ sơ</string>\n    <string name=\"preference_app_theme_green\">Màu lục</string>\n    <string name=\"preference_media_item_size_small\">Nhỏ</string>\n    <string name=\"preference_media_item_size_large\">Lớn</string>\n    <string name=\"preference_start_fragment_home\">Trang chủ</string>\n    <string name=\"preference_start_fragment_library\">Thư viện</string>\n    <string name=\"preference_remember_search_filters\">Ghi nhớ bộ lọc tìm kiếm</string>\n    <string name=\"library_not_started\">Chưa bắt đầu</string>\n    <string name=\"library_not_logged_in_title\">Đăng nhập để truy cập thư viện của bạn</string>\n    <string name=\"library_kind_all\">Tất cả</string>\n    <string name=\"library_not_synchronized\">Chưa được đồng bộ hóa</string>\n    <string name=\"library_edit_status\">Trạng thái thư viện</string>\n    <string name=\"profile_stats_manga_chapters_read\">%d chương đã đọc.</string>\n    <string name=\"profile_stats_time_spent_total\">Tổng cộng là %s.</string>\n    <string name=\"profile_stats_completed\">%1$d đã hoàn thành. Hơn &lt;b&gt;%2$d%%&lt;/b&gt; người dùng.</string>\n    <string name=\"profile_data_join_date\">Ngày gia nhập</string>\n    <string name=\"profile_data_join_date_ago\">%s trước</string>\n    <string name=\"profile_gender_male\">Nam giới</string>\n    <string name=\"profile_gender_custom\">Tùy chỉnh</string>\n    <string name=\"profile_gender_custom_hint\">Xác định giới tính của bạn</string>\n    <string name=\"unit_episode\">Tập %d</string>\n    <string name=\"nav_home\">Trang chủ</string>\n    <string name=\"nav_search\">Tìm kiếm</string>\n    <string name=\"nav_library\">Thư viện</string>\n    <string name=\"nav_profile\">Hồ sơ</string>\n    <string name=\"nav_app_logs\">Nhật ký ứng dụng</string>\n    <string name=\"nav_os_libraries\">Thư viện nguồn mở</string>\n    <string name=\"action_cancel\">Hủy</string>\n    <string name=\"action_retry\">Thử lại</string>\n    <string name=\"action_reset_filter\">Đặt lại bộ lọc</string>\n    <string name=\"action_show_less\">Hiện ít hơn</string>\n    <string name=\"action_unselect_all\">Bỏ chọn tất cả</string>\n    <string name=\"action_read_more\">Đọc nhiều hơn</string>\n    <string name=\"action_read_less\">Đọc ít hơn</string>\n    <string name=\"action_show_more\">Hiện thêm</string>\n    <string name=\"action_back\">Trở lại</string>\n    <string name=\"action_log_out\">Đăng xuất</string>\n    <string name=\"action_share_profile_url\">Chia sẻ hồ sơ</string>\n    <string name=\"action_share_app_logs\">Chia sẻ nhật ký</string>\n    <string name=\"action_save_in_gallery\">Lưu vào phòng trưng bày</string>\n    <string name=\"action_save_changes\">Lưu thay đổi</string>\n    <string name=\"action_edit_profile\">Chỉnh sửa hồ sơ</string>\n    <string name=\"action_update\">Cập nhật</string>\n    <string name=\"action_start\">Bắt đầu</string>\n    <string name=\"action_allow\">Cho phép</string>\n    <string name=\"action_edit_profile_link\">Chỉnh sửa liên kết hồ sơ</string>\n    <string name=\"action_select_profile_link_site\">Chọn một trang</string>\n    <string name=\"action_delete_profile_link\">Xóa liên kết hồ sơ</string>\n    <string name=\"error_search_provider_unavailable\">Nhà cung cấp dịch vụ tìm kiếm không có sẵn.</string>\n    <string name=\"preference_start_fragment_search\">Tìm kiếm</string>\n    <string name=\"preference_remember_search_filters_description\">Áp dụng các bộ lọc tìm kiếm được sử dụng gần đây nhất sau khi khởi động lại ứng dụng.</string>\n    <string name=\"title_login\">Đăng nhập vào tài khoản Kitsu của bạn</string>\n    <string name=\"title_play_trailer\">Phát đoạn giới thiệu</string>\n    <string name=\"title_about_me\">Về tôi</string>\n    <string name=\"data_length_total\">%s tổng cộng</string>\n    <string name=\"data_title_japanese\">Tiếng Nhật</string>\n    <string name=\"data_title_aired\">Đã phát sóng</string>\n    <string name=\"data_title_chapters\">Chương</string>\n    <string name=\"data_title_episodes\">Tập</string>\n    <string name=\"library_edit_volumes\">Cuốn</string>\n    <string name=\"data_title_producers\">Nhà sản xuất</string>\n    <string name=\"data_title_licensors\">Người cấp phép</string>\n    <string name=\"season_summer\">Mùa hè</string>\n    <string name=\"relationship_side_story\">Ngoại truyện</string>\n    <string name=\"relationship_full_story\">Truyện đầy đủ</string>\n    <string name=\"relationship_alternative_version\">Bản th.thế</string>\n    <string name=\"relationship_summary\">Bản tóm tắt</string>\n    <string name=\"relationship_adaptation\">Chuyển thể</string>\n    <string name=\"relationship_character\">Nhân vật</string>\n    <string name=\"relationship_other\">Khác</string>\n    <string name=\"section_top_upcoming_anime\">Anime sắp ra mắt hàng đầu</string>\n    <string name=\"preference_category_advanced\">Nâng cao</string>\n    <string name=\"preference_appearance_description\">Thay đổi chủ đề và chuyển đổi chế độ tối.</string>\n    <string name=\"preference_dark_mode\">Chế độ tối</string>\n    <string name=\"preference_oled_black_mode_description\">Sử dụng nền đen ở chế độ tối.</string>\n    <string name=\"preference_app_theme\">Chủ đề</string>\n    <string name=\"preference_dynamic_color_theme\">Sử dụng màu linh động</string>\n    <string name=\"preference_app_theme_default\">Mặc định</string>\n    <string name=\"preference_app_theme_purple\">Màu tím</string>\n    <string name=\"preference_media_item_size\">Kích thước ảnh áp phích</string>\n    <string name=\"preference_media_item_size_medium\">Vừa vừa</string>\n    <string name=\"preference_start_fragment\">Trang mặc định</string>\n    <string name=\"library_not_logged_in_text\">Đăng nhập vào tài khoản Kitsu của bạn để quản lý thư viện của bạn và có quyền truy cập vào tất cả các tính năng.</string>\n    <string name=\"library_edit_progress\">Tiến độ</string>\n    <string name=\"status_tba\">TBA</string>\n    <string name=\"character_role_recurring\">Định kỳ</string>\n    <string name=\"character_role_cameo\">Khách mời</string>\n    <string name=\"data_title_tba\">Dự kiến</string>\n    <string name=\"data_title_serialization\">Tuần tự</string>\n    <string name=\"relationship_alternative_setting\">Thiết đặt th.thế</string>\n    <string name=\"relationship_spinoff\">Sản phẩm phụ</string>\n    <string name=\"preference_start_fragment_description\">Trang mặc định được đặt thành %s.</string>\n    <string name=\"preference_language\">Ngôn ngữ</string>\n    <string name=\"preference_display_name\">Tên hiển thị</string>\n    <string name=\"preference_country\">Quốc gia</string>\n    <string name=\"preference_titles\">Tiêu đề</string>\n    <string name=\"preference_titles_description\">Xác định cách hiển thị tiêu đề phương tiện.</string>\n    <string name=\"preference_titles_canonical\">Nguyên mẫu</string>\n    <string name=\"preference_titles_romanized\">Latin hóa</string>\n    <string name=\"preference_titles_english\">Tiếng Anh</string>\n    <string name=\"preference_country_summary_non\">Chưa chỉ định quốc gia.</string>\n    <string name=\"preference_adult_content_description_sfw\">Phương tiện có xếp hạng độ tuổi R18+ sẽ bị ẩn.</string>\n    <string name=\"preference_rating_system_simple\">Đơn giản</string>\n    <string name=\"preference_rating_system_regular\">Thông thường</string>\n    <string name=\"preference_rating_system_advanced\">Nâng cao</string>\n    <string name=\"preference_force_legacy_image_picker\">Buộc bộ chọn ảnh kiểu cũ</string>\n    <string name=\"preference_app_logs_description\">Xem và chia sẻ tin nhắn nhật ký ứng dụng.</string>\n    <string name=\"preference_app_version\">Thông tin phiên bản</string>\n    <string name=\"preference_app_version_description\">Nhấn để kiểm tra các bản cập nhật.</string>\n    <string name=\"preference_github_repo\">Kho lưu trữ GitHub</string>\n    <string name=\"action_log_in\">Đăng nhập</string>\n    <string name=\"invalid_username\">Không phải là một địa chỉ e-mail hợp lệ</string>\n    <string name=\"login_no_account\">Không có tài khoản? Tạo một cái trên <a href=\"https://kitsu.app\">kitsu.app</a>.</string>\n    <string name=\"library_edit_start_rewatch\">Bắt đầu xem lại</string>\n    <string name=\"library_edit_no_date_set\">Chưa đặt ngày</string>\n    <string name=\"library_edit_notes\">Ghi chú cá nhân</string>\n    <string name=\"info_update_new_version_available_text\">Đã có phiên bản %s của Kitsune.</string>\n    <string name=\"info_update_failed\">Không thể kiểm tra phiên bản mới.</string>\n    <string name=\"filter_year\">Năm</string>\n    <string name=\"filter_avg_rating\">Xếp hạng trung bình</string>\n    <string name=\"filter_select_categories\">Chọn danh mục</string>\n    <string name=\"filter_streamers\">Nguồn phát</string>\n    <string name=\"profile_not_logged_in_title\">Không có gì để thấy ở đây!</string>\n    <string name=\"profile_data_waifu\">Vợ ảo</string>\n    <string name=\"profile_data_husbando\">Chồng ảo</string>\n    <string name=\"profile_waifu_or_husbando\">Vợ ảo hoặc Chồng ảo</string>\n    <string name=\"notification_updates\">Cập nhật ứng dụng</string>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"other\">%s giây</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"other\">%s phút</item>\n    </plurals>\n    <string name=\"preference_adult_content_description_sometimes\">Phương tiện có xếp hạng độ tuổi R18+ sẽ hiển thị nhưng các bài đăng được gắn thẻ NSFW sẽ bị ẩn trong nguồn cấp dữ liệu toàn cầu.</string>\n    <string name=\"preference_adult_content_description_everywhere\">Phương tiện có xếp hạng độ tuổi R18+ sẽ hiển thị ở mọi nơi.</string>\n    <string name=\"preference_sfw_filter_hide_all\">Ẩn tất cả nội dung người lớn</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">Giới hạn theo dõi nguồn cấp dữ liệu</string>\n    <string name=\"preference_sfw_filter_show_all\">Nội dung người lớn ở mọi nơi</string>\n    <string name=\"preference_rating_system\">Hệ thống xếp hạng</string>\n    <string name=\"preference_force_legacy_image_picker_description\">Sử dụng bộ chọn tệp hệ thống thay vì bộ chọn ảnh mới.</string>\n    <string name=\"preference_open_source_libraries_description\">Một danh sách mọi thư viện nguồn mở đã dùng.</string>\n    <string name=\"preference_check_for_updates\">Kiểm tra cập nhật khi khởi chạy</string>\n    <string name=\"preference_check_for_updates_description\">Kiểm tra phiên bản ứng dụng mới mỗi lần khởi chạy.</string>\n    <string name=\"preference_not_logged_in\">Bạn phải đăng nhập để chỉnh sửa thiết đặt này.</string>\n    <string name=\"app_logs_no_data\">Không có sẵn nhật ký.</string>\n    <string name=\"prompt_email\">Email</string>\n    <string name=\"action_log_in_to_kitsu\">Đăng nhập vào Kitsu</string>\n    <string name=\"info_log_in_required\">Đăng nhập để truy cập tất cả các tính năng.</string>\n    <string name=\"prompt_password\">Mật khẩu</string>\n    <string name=\"login_failed\">Đăng nhập thất bại</string>\n    <string name=\"action_sign_in\">Đăng nhập</string>\n    <string name=\"status_logging_in\">Đang đăng nhập…</string>\n    <string name=\"logged_in_success\">Đã đăng nhập với tên %s.</string>\n    <string name=\"not_logged_in\">Chưa đăng nhập</string>\n    <string name=\"info_image_saved_in_gallery\">Đã lưu thành công hình ảnh vào phòng trưng bày.</string>\n    <string name=\"filter_season\">Mùa</string>\n    <string name=\"filter_age_rating\">Xếp hạng độ tuổi</string>\n    <string name=\"info_update_checking_new_version\">Đang kiểm tra phiên bản mới…</string>\n    <string name=\"unit_chapter\">Chương %d</string>\n    <string name=\"info_update_new_version_available\">Có sẵn phiên bản mới.</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"other\">%s Trang</item>\n    </plurals>\n    <string name=\"info_update_no_new_version_available\">Không có sẵn phiên bản mới.</string>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"other\">%s giờ</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"other\">%s ngày</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"other\">%s tháng</item>\n    </plurals>\n    <string name=\"info_logged_out_token_expired\">Mã thông báo truy cập của bạn đã hết hạn. Xin vui lòng đăng nhập lại.</string>\n    <string name=\"filter_subtype\">Loại phụ</string>\n    <plurals name=\"duration_years\">\n        <item quantity=\"other\">%s năm</item>\n    </plurals>\n    <string name=\"filter_kind\">Loại</string>\n    <string name=\"preference_country_summary\">Quốc gia được đặt thành %s</string>\n    <string name=\"preference_adult_content\">Nội dung người lớn</string>\n    <string name=\"library_edit_rating\">Xếp hạng</string>\n    <string name=\"library_edit_privacy_public\">Công cộng</string>\n    <string name=\"library_edit_reread_count\">Số lần đọc lại</string>\n    <string name=\"library_edit_start_reread\">Bắt đầu đọc lại</string>\n    <string name=\"library_edit_rewatch_count\">Số lần xem lại</string>\n    <string name=\"library_edit_privacy\">Riêng tư</string>\n    <string name=\"library_edit_privacy_private\">Riêng tư</string>\n    <string name=\"library_edit_started\">Bắt đầu</string>\n    <string name=\"library_edit_finished\">Hoàn thành</string>\n    <string name=\"preference_language_default\">ngôn ngữ mặc định</string>\n    <string name=\"onboarding_setup_title_language_selected\">Đã chọn: %1$s</string>\n    <string name=\"onboarding_setup_action\">Hoàn tất thiết lập</string>\n    <string name=\"onboarding_welcome_feature_explore\">Khám phá</string>\n    <string name=\"onboarding_welcome_feature_explore_description\">Khám phá những tựa sách mới và đang thịnh hành.</string>\n    <string name=\"onboarding_login_subtitle\">Đăng nhập vào tài khoản Kitsu của bạn hoặc tạo tài khoản mới để có quyền truy cập vào tất cả các tính năng.</string>\n    <string name=\"onboarding_login_forward_dialog_title\">Tạo tài khoản</string>\n    <string name=\"onboarding_welcome_feature_track_description\">Theo dõi những gì bạn đã xem và đọc.</string>\n    <string name=\"action_skip\">Bỏ qua</string>\n    <string name=\"action_next\">Kế tiếp</string>\n    <string name=\"action_continue\">Tiếp tục</string>\n    <string name=\"action_create_account\">Tạo tài khoản</string>\n    <string name=\"widget_description\">Tiến trình thư viện</string>\n    <string name=\"widget_empty_text\">Không có mục theo dõi đang hoạt động nào trong thư viện của bạn</string>\n    <string name=\"onboarding_welcome_title\">Chào mừng đến với Kitsune!</string>\n    <string name=\"onboarding_welcome_subtitle\">Người bạn đồng hành anime và manga của bạn.</string>\n    <string name=\"onboarding_welcome_feature_search\">Tìm kiếm</string>\n    <string name=\"onboarding_welcome_feature_search_description\">Tìm anime và manga yêu thích của bạn.</string>\n    <string name=\"widget_empty_action\">Thư viện mở</string>\n    <string name=\"onboarding_welcome_action\">Bắt đầu</string>\n    <string name=\"onboarding_login_title\">Đăng nhập vào Kitsu</string>\n    <string name=\"onboarding_login_is_logged_in\">Bạn đã đăng nhập</string>\n    <string name=\"onboarding_login_forward_dialog_message\">Bạn sẽ được chuyển hướng đến %1$s nơi bạn có thể tạo tài khoản mới.</string>\n    <string name=\"onboarding_setup_title\">Thiết lập ban đầu</string>\n    <string name=\"onboarding_setup_subtitle\">Thiết lập cài đặt ưa thích của bạn để bắt đầu. Bạn có thể thay đổi chúng bất cứ lúc nào sau đó trong phần cài đặt.</string>\n    <string name=\"onboarding_setup_notification_permission_notice\">Cần phải có quyền thông báo.</string>\n    <string name=\"onboarding_setup_updates\">Kiểm tra Cập nhật</string>\n    <string name=\"onboarding_setup_updates_description\">Nhận thông báo khi có bản phát hành mới trên GitHub.</string>\n    <string name=\"onboarding_setup_title_language\">Ngôn ngữ Tiêu đề</string>\n    <string name=\"onboarding_setup_title_language_description\">Chọn ngôn ngữ bạn muốn cho tiêu đề.</string>\n    <string name=\"onboarding_welcome_feature_track\">Theo dõi</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-w1240dp/dimens.xml",
    "content": "<resources>\n    <dimen name=\"activity_horizontal_margin\">200dp</dimen>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"preference_appearance_description\">更改主题并切换深色模式。</string>\n    <string name=\"action_add_profile_link\">添加个人主页链接</string>\n    <string name=\"error_user_delete_waifu_failed\">删除老婆/老公失败。</string>\n    <string name=\"title_more_franchise\">本系列的更多内容</string>\n    <string name=\"action_show_less\">收起</string>\n    <string name=\"preference_start_fragment_description\">默认页面设置为 %s。</string>\n    <string name=\"dialog_notification_permission_rejected\">你不会收到通知，也不会收到应用更新的通知。</string>\n    <string name=\"error_user_create_profile_link_failed\">创建 %s 个人主页链接失败。</string>\n    <string name=\"preference_titles_description\">定义媒体标题的显示方式。</string>\n    <string name=\"error_library_update_failed\">无法更新库条目。</string>\n    <string name=\"preference_app_version_description\">点击检查更新。</string>\n    <string name=\"action_share_app_logs\">分享日志</string>\n    <string name=\"action_save_changes\">保存更改</string>\n    <string name=\"dialog_log_out_confirmation\">您确定要退出登录吗？</string>\n    <string name=\"preference_adult_content_description_sometimes\">带有 R18+ 年龄等级的媒体可见，但被标记为 NSFW 的帖子会被隐藏在全局信息流中。</string>\n    <string name=\"action_log_in_to_kitsu\">登录Kitsu</string>\n    <string name=\"data_popularity\">人气#%d</string>\n    <string name=\"library_edit_rewatch_count\">重看次数</string>\n    <string name=\"preference_check_for_updates_description\">在每次启动时检查新的应用版本。</string>\n    <string name=\"sort_average_rating_asc\">评分稀烂</string>\n    <string name=\"profile_waifu_or_husbando\">老婆/老公</string>\n    <string name=\"login_no_account\">没有帐户？在<a href=\"https://kitsu.app\">kitsu.app</a>上创建一个。</string>\n    <string name=\"info_image_saved_in_gallery\">图片保存图库成功。</string>\n    <string name=\"info_logged_out_token_expired\">您的访问令牌已过期。请重新登录。</string>\n    <plurals name=\"duration_hours\">\n        <item quantity=\"other\">%s 小时</item>\n    </plurals>\n    <string name=\"title_edit_profile\">编辑个人资料</string>\n    <string name=\"data_title_published\">已出版</string>\n    <string name=\"library_status_watching\">在看</string>\n    <string name=\"section_top_upcoming_anime\">即将上映的热门动漫</string>\n    <string name=\"character_role_supporting\">配角</string>\n    <string name=\"data_kitsu_score\">Kitsu分数%s%%</string>\n    <string name=\"section_top_airing_manga\">顶级出版漫画</string>\n    <string name=\"season_summer\">夏季</string>\n    <string name=\"nav_home\">主页</string>\n    <string name=\"nav_search\">搜索</string>\n    <string name=\"nav_library\">库</string>\n    <string name=\"nav_profile\">账号</string>\n    <string name=\"nav_settings\">设置</string>\n    <string name=\"nav_appearance\">外观</string>\n    <string name=\"nav_app_logs\">应用程序日志</string>\n    <string name=\"nav_os_libraries\">开源库</string>\n    <string name=\"anime\">动漫</string>\n    <string name=\"manga\">漫画</string>\n    <string name=\"action_retry\">重试</string>\n    <string name=\"action_ok\">确定</string>\n    <string name=\"action_cancel\">取消</string>\n    <string name=\"action_reset_filter\">重置过滤器</string>\n    <string name=\"action_unselect_all\">取消全选</string>\n    <string name=\"action_read_more\">阅读更多</string>\n    <string name=\"action_read_less\">收起</string>\n    <string name=\"action_show_more\">展开</string>\n    <string name=\"action_view\">查看</string>\n    <string name=\"action_back\">返回</string>\n    <string name=\"action_close\">关闭</string>\n    <string name=\"action_log_out\">登出</string>\n    <string name=\"action_share_profile_url\">分享个人主页</string>\n    <string name=\"action_share\">分享</string>\n    <string name=\"action_open_external\">在外部网站打开</string>\n    <string name=\"action_open_external_short\">外部网站</string>\n    <string name=\"action_add_to_favorites\">添加到收藏夹</string>\n    <string name=\"action_remove_from_favorites\">从收藏夹中移除</string>\n    <string name=\"action_dismiss\">忽略</string>\n    <string name=\"action_synchronize\">同步</string>\n    <string name=\"action_save_in_gallery\">保存到图库</string>\n    <string name=\"action_open_in_browser\">在浏览器中打开</string>\n    <string name=\"action_open_on_mal\">在 MyAnimeList.net 上打开</string>\n    <string name=\"action_edit_profile\">编辑个人资料</string>\n    <string name=\"action_update_profile\">更新个人资料</string>\n    <string name=\"action_add\">添加</string>\n    <string name=\"action_remove\">移除</string>\n    <string name=\"action_update\">更新</string>\n    <string name=\"action_start\">开始</string>\n    <string name=\"action_allow\">允许</string>\n    <string name=\"action_rate\">评分</string>\n    <string name=\"action_update_rating\">更新评分</string>\n    <string name=\"action_remove_rating\">移除评分</string>\n    <string name=\"action_select_profile_link_site\">选择网站</string>\n    <string name=\"hint_search\">搜索</string>\n    <string name=\"action_edit_profile_link\">编辑个人主页链接</string>\n    <string name=\"action_delete_profile_link\">删除个人主页链接</string>\n    <string name=\"hint_mark_watched\">标记为看过。</string>\n    <string name=\"hint_mark_not_watched\">标记为未看。</string>\n    <string name=\"hint_rating\">评分</string>\n    <string name=\"dialog_remove_from_library_title\">从库中删除?</string>\n    <string name=\"dialog_remove_from_library_msg\">&lt;b&gt;%s&lt;/b&gt;将从库中删除。</string>\n    <string name=\"dialog_notification_permission_rejected_title\">权限被拒绝</string>\n    <string name=\"dialog_request_notification_permission_title\">允许通知</string>\n    <string name=\"dialog_request_notification_permission\">允许在新应用版本发布时收到推送通知。</string>\n    <string name=\"error_resource_loading\">无法加载数据。</string>\n    <string name=\"error_image_loading\">加载图像失败。</string>\n    <string name=\"error_nothing_found\">神马都没找到。</string>\n    <string name=\"error_invalid_user\">无效的用户。你登录了吗？</string>\n    <string name=\"error_user_update_failed\">更新用户数据失败。</string>\n    <string name=\"hint_search_characters\">搜索角色</string>\n    <string name=\"error_user_update_image_failed\">更新个人头像或封面失败。</string>\n    <string name=\"error_user_update_profile_link_failed\">无法更新%s的配置文件链接。</string>\n    <string name=\"error_user_delete_profile_link_failed\">无法删除 %s 的个人主页链接。</string>\n    <string name=\"error_something_wrong\">哦豁，出问题了！</string>\n    <string name=\"error_requires_external_storage_permission\">需要写入外部存储的权限。</string>\n    <string name=\"error_search_provider_unavailable\">搜索提供者不可用。</string>\n    <string name=\"error_library_update_failed_multiple\">无法更新%d个库条目。</string>\n    <string name=\"error_library_update_not_found\">库条目不存在。</string>\n    <string name=\"error_library_add_failed\">添加到您的库失败。</string>\n    <string name=\"error_library_delete_failed\">删除库条目失败。</string>\n    <string name=\"error_requires_notification_permission\">需要通知权限。</string>\n    <string name=\"sort_popularity_desc\">最热门</string>\n    <string name=\"sort_popularity_asc\">最冷门</string>\n    <string name=\"sort_average_rating_desc\">最高评分</string>\n    <string name=\"sort_release_date_asc\">最旧</string>\n    <string name=\"sort_release_date_desc\">最新</string>\n    <string name=\"title_media_type\">媒体类型</string>\n    <string name=\"title_categories\">类别</string>\n    <string name=\"title_filter\">筛选</string>\n    <string name=\"title_description\">描述</string>\n    <string name=\"title_details\">详情</string>\n    <string name=\"title_streamer\">流媒体链接</string>\n    <string name=\"title_ratings\">评分</string>\n    <string name=\"title_favorite_anime\">最喜欢的动漫</string>\n    <string name=\"title_favorite_manga\">最喜欢的漫画</string>\n    <string name=\"title_favorite_characters\">最喜爱的角色</string>\n    <string name=\"title_authentication\">验证</string>\n    <string name=\"title_login\">登录您的Kitsu帐户</string>\n    <string name=\"title_play_trailer\">播放预告片</string>\n    <string name=\"title_about_me\">关于我</string>\n    <string name=\"title_episodes\">集</string>\n    <string name=\"title_chapters\">章</string>\n    <string name=\"title_characters\">角色</string>\n    <string name=\"title_edit_library_entry\">编辑库条目</string>\n    <string name=\"no_information\">未知</string>\n    <string name=\"no_data_available\">无可用数据</string>\n    <string name=\"status_current\">正在播出</string>\n    <string name=\"status_finished\">已完结</string>\n    <string name=\"status_tba\">待公布</string>\n    <string name=\"status_unreleased\">未发行</string>\n    <string name=\"status_upcoming\">即将到来</string>\n    <string name=\"character_role_main\">主角</string>\n    <string name=\"title_character_appearances\">角色出场</string>\n    <string name=\"status_current_manga\">出版中</string>\n    <string name=\"character_role_recurring\">经常出镜</string>\n    <string name=\"character_role_cameo\">客串</string>\n    <string name=\"library_status_planned\">想看</string>\n    <string name=\"library_status_completed\">看过</string>\n    <string name=\"library_status_on_hold\">搁置</string>\n    <string name=\"library_status_dropped\">抛弃</string>\n    <string name=\"library_status_reading\">在读</string>\n    <string name=\"library_status_planned_manga\">想读</string>\n    <string name=\"library_action_remove\">从库中删除</string>\n    <string name=\"library_action_add\">添加到库</string>\n    <string name=\"library_action_edit\">编辑库条目</string>\n    <string name=\"data_rank\">排名 #%d</string>\n    <string name=\"data_length_total\">总计 %s</string>\n    <string name=\"data_length_each\">每集 %d 分钟</string>\n    <string name=\"data_title_english\">英文</string>\n    <string name=\"data_title_japanese\">日文</string>\n    <string name=\"data_title_japanese_romaji\">日文（罗马拼音）</string>\n    <string name=\"data_other_names\">别名</string>\n    <string name=\"data_abbreviated_titles\">同义词</string>\n    <string name=\"data_title_type\">类型</string>\n    <string name=\"data_title_status\">状态</string>\n    <string name=\"data_title_aired\">已播出</string>\n    <string name=\"data_title_tba\">预期</string>\n    <string name=\"data_title_season\">季</string>\n    <string name=\"data_title_age_rating\">评分</string>\n    <string name=\"data_title_serialization\">序列化</string>\n    <string name=\"data_title_chapters\">章</string>\n    <string name=\"data_title_volumes\">卷</string>\n    <string name=\"data_title_episodes\">集</string>\n    <string name=\"data_title_length\">长度</string>\n    <string name=\"data_title_producers\">制片人</string>\n    <string name=\"data_title_licensors\">许可方</string>\n    <string name=\"data_title_studios\">工作室</string>\n    <string name=\"season_winter\">冬季</string>\n    <string name=\"season_spring\">春季</string>\n    <string name=\"season_fall\">秋季</string>\n    <string name=\"relationship_sequel\">续集</string>\n    <string name=\"relationship_prequel\">前传</string>\n    <string name=\"relationship_side_story\">支线</string>\n    <string name=\"relationship_parent_story\">前传</string>\n    <string name=\"relationship_summary\">摘要</string>\n    <string name=\"relationship_full_story\">全集</string>\n    <string name=\"relationship_spinoff\">衍伸</string>\n    <string name=\"relationship_adaptation\">改编</string>\n    <string name=\"relationship_character\">角色</string>\n    <string name=\"relationship_other\">其他</string>\n    <string name=\"section_highest_rated_anime\">评分最高的动漫</string>\n    <string name=\"section_most_popular_anime\">最受欢迎的动漫</string>\n    <string name=\"section_top_upcoming_manga\">热门即将推出的漫画</string>\n    <string name=\"section_highest_rated_manga\">评分最高的漫画</string>\n    <string name=\"section_trending\">本周趋势</string>\n    <string name=\"section_most_popular_manga\">最受欢迎的漫画</string>\n    <string name=\"section_top_airing_anime\">热播动漫</string>\n    <string name=\"preference_category_ui\">用户界面</string>\n    <string name=\"preference_category_account\">账户</string>\n    <string name=\"preference_category_advanced\">高级</string>\n    <string name=\"preference_category_about\">关于</string>\n    <string name=\"preference_dark_mode\">深色模式</string>\n    <string name=\"preference_oled_black_mode\">OLED 黑</string>\n    <string name=\"preference_oled_black_mode_description\">在深色模式下使用黑色背景。</string>\n    <string name=\"preference_app_theme\">主题</string>\n    <string name=\"preference_dynamic_color_theme\">使用动态颜色</string>\n    <string name=\"preference_dynamic_color_theme_description\">适应系统主题。</string>\n    <string name=\"preference_app_theme_default\">默认</string>\n    <string name=\"preference_app_theme_green\">绿</string>\n    <string name=\"preference_app_theme_purple\">紫</string>\n    <string name=\"preference_app_theme_blue\">蓝</string>\n    <string name=\"preference_media_item_size\">海报图片尺寸</string>\n    <string name=\"preference_start_fragment\">默认页面</string>\n    <string name=\"preference_language\">语言</string>\n    <string name=\"preference_display_name\">显示名称</string>\n    <string name=\"preference_profile_url\">个人资料URL</string>\n    <string name=\"preference_profile_url_not_set\">未设置个人资料 URL。</string>\n    <string name=\"preference_country\">国家/地区</string>\n    <string name=\"preference_country_summary\">国家/地区设置为%s</string>\n    <string name=\"preference_country_summary_non\">未指定国家/地区。</string>\n    <string name=\"preference_titles\">标题</string>\n    <string name=\"preference_adult_content\">成人内容</string>\n    <string name=\"preference_adult_content_description_sfw\">年龄分级为R18 +的媒体将被隐藏。</string>\n    <string name=\"preference_adult_content_description_everywhere\">R18+年龄分级的媒体将随处可见。</string>\n    <string name=\"preference_rating_system\">评分系统</string>\n    <string name=\"preference_remember_search_filters\">记住搜索筛选条件</string>\n    <string name=\"preference_remember_search_filters_description\">程序重启后使用上次使用过的搜索筛选规则。</string>\n    <string name=\"preference_force_legacy_image_picker\">强制旧式照片选择器</string>\n    <string name=\"preference_force_legacy_image_picker_description\">使用系统文件选择器，而不是新的照片选择器。</string>\n    <string name=\"preference_check_for_updates\">检查发布时的更新</string>\n    <string name=\"preference_app_logs_description\">查看并共享应用程序日志消息。</string>\n    <string name=\"preference_app_version\">版本信息</string>\n    <string name=\"preference_github_repo\">GitHub 代码库</string>\n    <string name=\"preference_open_source_libraries_description\">所有已使用的开源库的列表。</string>\n    <string name=\"preference_not_logged_in\">您必须登录才能编辑此设置。</string>\n    <string name=\"app_logs_no_data\">没有可用的日志。</string>\n    <string name=\"prompt_email\">电子邮件</string>\n    <string name=\"prompt_password\">密码</string>\n    <string name=\"action_sign_in\">登录</string>\n    <string name=\"action_log_in\">登录</string>\n    <string name=\"invalid_username\">不是有效的邮箱地址</string>\n    <string name=\"login_failed\">登录失败</string>\n    <string name=\"status_logging_in\">正在登录 …</string>\n    <string name=\"logged_in_success\">以%s登录。</string>\n    <string name=\"not_logged_in\">未登录</string>\n    <string name=\"library_not_started\">未开始</string>\n    <string name=\"library_not_rated\">未打分</string>\n    <string name=\"library_not_logged_in_title\">登录以访问您的库</string>\n    <string name=\"library_not_logged_in_text\">登录您的Kitsu帐户以管理您的库并访问所有功能。</string>\n    <string name=\"library_kind_all\">所有</string>\n    <string name=\"library_not_synchronized\">未同步</string>\n    <string name=\"library_edit_status\">库状态</string>\n    <string name=\"library_edit_progress\">进度</string>\n    <string name=\"library_edit_volumes\">卷</string>\n    <string name=\"library_edit_rating\">评分</string>\n    <string name=\"library_edit_reread_count\">重读次数</string>\n    <string name=\"library_edit_start_rewatch\">开始重看</string>\n    <string name=\"library_edit_start_reread\">开始重读</string>\n    <string name=\"library_edit_privacy\">隐私</string>\n    <string name=\"library_edit_privacy_public\">公开</string>\n    <string name=\"library_edit_privacy_private\">仅自己可见</string>\n    <string name=\"library_edit_started\">开始于</string>\n    <string name=\"library_edit_finished\">完成于</string>\n    <string name=\"library_edit_no_date_set\">未设置日期</string>\n    <string name=\"library_edit_notes\">个人笔记</string>\n    <string name=\"info_log_in_required\">登录畅享所有功能。</string>\n    <string name=\"info_update_checking_new_version\">检查新版本…</string>\n    <string name=\"info_update_new_version_available\">有新版本。</string>\n    <string name=\"info_update_new_version_available_text\">Kitsune %s版本可用。</string>\n    <string name=\"info_update_no_new_version_available\">木有新版本。</string>\n    <string name=\"info_update_failed\">检查新版本失败。</string>\n    <string name=\"filter_kind\">种类</string>\n    <string name=\"filter_year\">年</string>\n    <string name=\"filter_avg_rating\">平均评分</string>\n    <string name=\"filter_select_categories\">选择类别</string>\n    <string name=\"filter_season\">季</string>\n    <string name=\"filter_subtype\">子类型</string>\n    <string name=\"filter_streamers\">流媒体平台</string>\n    <string name=\"filter_age_rating\">年龄分级</string>\n    <string name=\"profile_not_logged_in_title\">这里没什么可看的!</string>\n    <string name=\"profile_not_logged_in_text\">登录您的Kitsu帐户或创建一个新帐户以纵享所有功能。</string>\n    <string name=\"profile_anime_stats\">动漫统计</string>\n    <string name=\"profile_manga_stats\">漫画统计</string>\n    <string name=\"profile_stats_anime_watch_time\">%s 花在了看动漫上。</string>\n    <string name=\"profile_stats_manga_chapters_read\">读了 %d 章。</string>\n    <string name=\"profile_stats_time_spent_total\">总计 %s。</string>\n    <string name=\"profile_stats_completed\">%1$d已完成。超过&lt;b&gt;%2$d%%&lt;/b&gt;的用户。</string>\n    <string name=\"profile_data_gender\">性别</string>\n    <string name=\"profile_data_location\">位置</string>\n    <string name=\"profile_data_birthday\">生日</string>\n    <string name=\"profile_data_join_date\">加入日期</string>\n    <string name=\"profile_data_join_date_ago\">%s 前</string>\n    <string name=\"profile_data_private\">这是个秘密。</string>\n    <string name=\"profile_gender_male\">男</string>\n    <string name=\"profile_gender_female\">女</string>\n    <string name=\"profile_gender_custom\">自定义</string>\n    <string name=\"profile_gender_custom_hint\">定义你的性别</string>\n    <string name=\"profile_data_waifu\">老婆</string>\n    <string name=\"profile_data_husbando\">老公</string>\n    <string name=\"profile_find_waifu\">找到您的另一半</string>\n    <string name=\"profile_data_bio\">简介</string>\n    <string name=\"profile_cover_image_description\">封面图</string>\n    <string name=\"profile_avatar_image_description\">个人资料图片</string>\n    <string name=\"profile_link_url\">URL或用户名</string>\n    <string name=\"notification_updates\">应用程序更新</string>\n    <string name=\"unit_episode\">第%d集</string>\n    <string name=\"unit_chapter\">第%d章</string>\n    <plurals name=\"unit_pages\">\n        <item quantity=\"other\">%s 页</item>\n    </plurals>\n    <plurals name=\"duration_seconds\">\n        <item quantity=\"other\">%s 秒</item>\n    </plurals>\n    <plurals name=\"duration_minutes\">\n        <item quantity=\"other\">%s 分钟</item>\n    </plurals>\n    <plurals name=\"duration_days\">\n        <item quantity=\"other\">%s 天</item>\n    </plurals>\n    <plurals name=\"duration_months\">\n        <item quantity=\"other\">%s 月</item>\n    </plurals>\n    <plurals name=\"duration_years\">\n        <item quantity=\"other\">%s 年</item>\n    </plurals>\n    <string name=\"relationship_alternative_version\">不同版本</string>\n    <string name=\"error_mapping_loading\">未能加载外部站点的映射。</string>\n    <string name=\"preference_rating_system_advanced\">高级</string>\n    <string name=\"preference_dark_mode_light\">浅色</string>\n    <string name=\"relationship_alternative_setting\">不同场景</string>\n    <string name=\"preference_dark_mode_dark\">深色</string>\n    <string name=\"preference_dark_mode_follow_system\">跟随系统</string>\n    <string name=\"preference_dark_mode_battery_saver\">节电模式</string>\n    <string name=\"preference_media_item_size_small\">小</string>\n    <string name=\"preference_media_item_size_medium\">中</string>\n    <string name=\"preference_media_item_size_large\">大</string>\n    <string name=\"preference_start_fragment_home\">主页</string>\n    <string name=\"preference_start_fragment_search\">搜索</string>\n    <string name=\"preference_start_fragment_library\">库</string>\n    <string name=\"preference_start_fragment_profile\">资料</string>\n    <string name=\"preference_titles_canonical\">规范名</string>\n    <string name=\"preference_titles_romanized\">罗马名</string>\n    <string name=\"preference_titles_english\">英文名</string>\n    <string name=\"preference_sfw_filter_hide_all\">隐藏所有成人内容</string>\n    <string name=\"preference_sfw_filter_limit_to_feed\">限制关注信息流</string>\n    <string name=\"preference_sfw_filter_show_all\">成人内容无处不在</string>\n    <string name=\"preference_rating_system_simple\">简单</string>\n    <string name=\"preference_rating_system_regular\">常规</string>\n    <string name=\"preference_language_default\">默认语言</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/xml/app_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:key=\"@string/preference_file_key\">\n\n    <PreferenceCategory\n        android:title=\"@string/preference_category_ui\"\n        app:iconSpaceReserved=\"false\">\n\n        <Preference\n            android:key=\"@string/preference_key_fragment_appearance\"\n            android:title=\"@string/nav_appearance\"\n            android:summary=\"@string/preference_appearance_description\"\n            app:iconSpaceReserved=\"false\" />\n\n        <ListPreference\n            android:key=\"@string/preference_key_language\"\n            android:title=\"@string/preference_language\"\n            app:useSimpleSummaryProvider=\"true\"\n            app:iconSpaceReserved=\"false\" />\n\n        <ListPreference\n            android:key=\"@string/preference_key_start_fragment\"\n            android:title=\"@string/preference_start_fragment\"\n            android:entries=\"@array/preference_start_fragment_entries\"\n            app:iconSpaceReserved=\"false\" />\n\n        <ListPreference\n            android:key=\"@string/preference_key_titles\"\n            android:title=\"@string/preference_titles\"\n            android:summary=\"@string/preference_titles_description\"\n            android:entries=\"@array/preference_titles_values\"\n            app:iconSpaceReserved=\"false\" />\n\n        <ListPreference\n            android:key=\"@string/preference_key_country\"\n            android:title=\"@string/preference_country\"\n            app:iconSpaceReserved=\"false\" />\n\n        <ListPreference\n            android:key=\"@string/preference_key_sfw_filter\"\n            android:title=\"@string/preference_adult_content\"\n            android:entries=\"@array/preference_sfw_filter_values\"\n            app:iconSpaceReserved=\"false\" />\n\n        <ListPreference\n            android:key=\"@string/preference_key_rating_system\"\n            android:title=\"@string/preference_rating_system\"\n            android:entries=\"@array/preference_rating_system_values\"\n            app:iconSpaceReserved=\"false\" />\n\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:title=\"@string/preference_category_account\"\n        app:iconSpaceReserved=\"false\">\n\n        <EditTextPreference\n            android:key=\"@string/preference_key_display_name\"\n            android:title=\"@string/preference_display_name\"\n            app:iconSpaceReserved=\"false\" />\n\n        <EditTextPreference\n            android:key=\"@string/preference_key_profile_url\"\n            android:title=\"@string/preference_profile_url\"\n            app:iconSpaceReserved=\"false\" />\n\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:title=\"@string/preference_category_advanced\"\n        app:iconSpaceReserved=\"false\">\n\n        <SwitchPreferenceCompat\n            android:key=\"@string/preference_key_remember_search_filters\"\n            android:title=\"@string/preference_remember_search_filters\"\n            android:summary=\"@string/preference_remember_search_filters_description\"\n            android:defaultValue=\"true\"\n            app:iconSpaceReserved=\"false\" />\n\n        <SwitchPreferenceCompat\n            android:key=\"@string/preference_key_force_legacy_image_picker\"\n            android:title=\"@string/preference_force_legacy_image_picker\"\n            android:summary=\"@string/preference_force_legacy_image_picker_description\"\n            android:defaultValue=\"false\"\n            app:iconSpaceReserved=\"false\" />\n\n        <SwitchPreferenceCompat\n            android:key=\"@string/preference_key_check_for_updates_on_start\"\n            android:title=\"@string/preference_check_for_updates\"\n            android:summary=\"@string/preference_check_for_updates_description\"\n            android:defaultValue=\"false\"\n            app:iconSpaceReserved=\"false\" />\n\n        <Preference\n            android:key=\"@string/preference_key_app_logs\"\n            android:title=\"@string/preference_app_logs\"\n            android:summary=\"@string/preference_app_logs_description\"\n            app:iconSpaceReserved=\"false\" />\n\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:title=\"@string/preference_category_about\"\n        app:iconSpaceReserved=\"false\">\n\n        <Preference\n            android:key=\"@string/preference_key_app_version\"\n            android:title=\"@string/preference_app_version\"\n            app:icon=\"@drawable/ic_outline_info_24\" />\n\n        <Preference\n            android:title=\"@string/preference_github_repo\"\n            android:summary=\"@string/github_repo_url\"\n            app:icon=\"@drawable/ic_github\">\n\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/github_repo_url\" />\n\n        </Preference>\n\n        <Preference\n            android:key=\"@string/preference_key_open_source_libraries\"\n            android:title=\"@string/preference_open_source_libraries\"\n            android:summary=\"@string/preference_open_source_libraries_description\"\n            app:icon=\"@drawable/ic_code_24\" />\n\n    </PreferenceCategory>\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/appearance_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:key=\"@string/preference_file_key\">\n\n    <SwitchPreferenceCompat\n        android:key=\"@string/preference_key_dynamic_color_theme\"\n        android:title=\"@string/preference_dynamic_color_theme\"\n        android:summary=\"@string/preference_dynamic_color_theme_description\"\n        app:iconSpaceReserved=\"false\" />\n\n    <io.github.drumber.kitsune.ui.settings.ThemePickerPreference\n        android:key=\"@string/preference_key_app_theme\"\n        android:title=\"@string/preference_app_theme\"\n        android:selectable=\"false\"\n        android:layout=\"@layout/layout_theme_picker_preference\" />\n\n    <ListPreference\n        android:key=\"@string/preference_key_dark_mode\"\n        android:title=\"@string/preference_dark_mode\"\n        android:entries=\"@array/preference_dark_mode_entries\"\n        android:entryValues=\"@array/preference_dark_mode_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:key=\"@string/preference_key_oled_black_mode\"\n        android:title=\"@string/preference_oled_black_mode\"\n        android:summary=\"@string/preference_oled_black_mode_description\"\n        app:iconSpaceReserved=\"false\" />\n\n    <ListPreference\n        android:key=\"@string/preference_key_media_item_size\"\n        android:title=\"@string/preference_media_item_size\"\n        android:entries=\"@array/preference_media_item_size_entries\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/backup_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<full-backup-content>\n    <include domain=\"sharedpref\" path=\".\" />\n    <exclude domain=\"sharedpref\" path=\"user_preferences.xml\" />\n    <exclude domain=\"sharedpref\" path=\"auth_preferences.xml\" />\n</full-backup-content>"
  },
  {
    "path": "app/src/main/res/xml/backup_rules_sdk31.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<data-extraction-rules>\n    <cloud-backup>\n        <include domain=\"sharedpref\" path=\".\" />\n        <exclude domain=\"sharedpref\" path=\"user_preferences.xml\" />\n        <exclude domain=\"sharedpref\" path=\"auth_preferences.xml\" />\n    </cloud-backup>\n    <device-transfer>\n        <include domain=\"sharedpref\" path=\".\" />\n        <exclude domain=\"sharedpref\" path=\"user_preferences.xml\" />\n        <exclude domain=\"sharedpref\" path=\"auth_preferences.xml\" />\n    </device-transfer>\n</data-extraction-rules>"
  },
  {
    "path": "app/src/main/res/xml/filepaths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <cache-path\n        name=\"logs\"\n        path=\"logs/\" />\n</paths>\n"
  },
  {
    "path": "app/src/main/res/xml/library_widget_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:description=\"@string/widget_description\"\n    android:initialLayout=\"@layout/glance_default_loading_layout\"\n    android:minWidth=\"160dp\"\n    android:minHeight=\"50dp\"\n    android:minResizeWidth=\"160dp\"\n    android:targetCellWidth=\"4\"\n    android:targetCellHeight=\"2\"\n    android:resizeMode=\"horizontal|vertical\"\n    android:updatePeriodMillis=\"86400000\"\n    android:widgetCategory=\"home_screen\" />"
  },
  {
    "path": "app/src/main/res/xml-v25/shortcuts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shortcuts xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <shortcut\n        android:icon=\"@drawable/ic_shortcut_library_24\"\n        android:shortcutId=\"library\"\n        android:shortcutShortLabel=\"@string/nav_library\">\n        <intent\n            android:action=\"io.github.drumber.kitsune.LIBRARY\"\n            android:targetClass=\"io.github.drumber.kitsune.ui.main.MainActivity\"\n            android:targetPackage=\"io.github.drumber.kitsune\" />\n    </shortcut>\n\n    <shortcut\n        android:icon=\"@drawable/ic_shortcut_search_24\"\n        android:shortcutId=\"search\"\n        android:shortcutShortLabel=\"@string/nav_search\">\n        <intent\n            android:action=\"io.github.drumber.kitsune.SEARCH\"\n            android:targetClass=\"io.github.drumber.kitsune.ui.main.MainActivity\"\n            android:targetPackage=\"io.github.drumber.kitsune\" />\n    </shortcut>\n\n    <shortcut\n        android:icon=\"@drawable/ic_shortcut_settings_24\"\n        android:shortcutId=\"settings\"\n        android:shortcutShortLabel=\"@string/nav_settings\">\n        <intent\n            android:action=\"io.github.drumber.kitsune.SETTINGS\"\n            android:targetClass=\"io.github.drumber.kitsune.ui.main.MainActivity\"\n            android:targetPackage=\"io.github.drumber.kitsune\" />\n    </shortcut>\n</shortcuts>"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/archunit/ArchUnitTest.kt",
    "content": "package io.github.drumber.kitsune.archunit\n\nimport com.tngtech.archunit.base.DescribedPredicate.not\nimport com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage\nimport com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage\nimport com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleNameEndingWith\nimport com.tngtech.archunit.core.importer.ClassFileImporter\nimport com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses\nimport com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS\nimport com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices\nimport io.github.drumber.kitsune.KitsuneApplication\nimport org.junit.Test\n\nclass ArchUnitTest {\n\n    companion object {\n        private val ALL_CLASSES =\n            ClassFileImporter().importPackagesOf(KitsuneApplication::class.java)\n\n        private val NON_TEST_CLASSES = ClassFileImporter().withImportOption {\n            !it.matches(\".*/(debug|release)UnitTest/.*\".toPattern())\n        }.importPackagesOf(KitsuneApplication::class.java)\n\n        private val TEST_CLASSES = ClassFileImporter().withImportOption {\n            it.matches(\".*/(debug|release)UnitTest/.*\".toPattern())\n        }.importPackagesOf(KitsuneApplication::class.java)\n    }\n\n    @Test\n    fun classes_should_not_use_standard_streams() {\n        NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS.check(ALL_CLASSES)\n    }\n\n    @Test\n    fun ui_should_not_depend_on_data_sources() {\n        noClasses()\n            .that()\n            .resideInAPackage(\"..kitsune.ui..\")\n            .should()\n            .dependOnClassesThat(\n                resideInAPackage(\"..data.source..\")\n                    .and(\n                        not(\n                            // exclusion: local user, character models and auth models are allowed in the UI\n                            resideInAnyPackage(\n                                \"..data.source.local.user.model..\",\n                                \"..data.source.local.character..\",\n                                \"..data.source.local.auth.model..\",\n                                // temporary exclusion: Algolia character search is used in EditProfileFragment\n                                \"..data.source.network.algolia.model.search..\"\n                            )\n                        )\n                    )\n            )\n            .because(\"UI classes should not depend on data sources directly\")\n            .check(NON_TEST_CLASSES)\n    }\n\n    @Test\n    fun local_and_network_data_sources_should_not_depend_on_each_other() {\n        slices()\n            .matching(\"..data.source.(*)..\")\n            .should()\n            .notDependOnEachOther()\n            .because(\"local and network data sources should be independent from each other\")\n            .check(NON_TEST_CLASSES)\n    }\n\n    @Test\n    fun other_layers_should_not_depend_on_data_sources_directly() {\n        noClasses()\n            .that()\n            .resideOutsideOfPackages(\"..data.source..\", \"..data.repository..\", \"..di..\")\n            .should()\n            .dependOnClassesThat(\n                resideInAPackage(\"..data.source..\")\n                    .and(simpleNameEndingWith(\"DataSource\"))\n            )\n            .because(\"data sources should only be accessed by the repositories\")\n            .check(NON_TEST_CLASSES)\n    }\n\n    @Test\n    fun presentation_model_classes_should_not_depend_on_data_sources() {\n        noClasses()\n            .that()\n            .resideInAPackage(\"..data.presentation.model..\")\n            .should()\n            .dependOnClassesThat()\n            .resideInAPackage(\"..data.source..\")\n            .because(\"presentation model classes should not depend on data sources\")\n            .check(NON_TEST_CLASSES)\n    }\n\n    @Test\n    fun common_classes_should_not_depend_on_data_sources() {\n        noClasses()\n            .that()\n            .resideInAPackage(\"..data.common..\")\n            .should()\n            .dependOnClassesThat()\n            .resideInAPackage(\"..data.source..\")\n            .because(\"common classes should not depend on data sources\")\n            .check(NON_TEST_CLASSES)\n    }\n\n    @Test\n    fun data_sources_should_not_depend_on_presentation_classes() {\n        noClasses()\n            .that()\n            .resideInAPackage(\"..data.source..\")\n            .should()\n            .dependOnClassesThat()\n            .resideInAPackage(\"..data.presentation..\")\n            .because(\"data sources classes should not depend on presentation classes\")\n            .check(NON_TEST_CLASSES)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/mapper/AuthMapperTest.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.mapper.AuthMapper.toLocalAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.NetworkAccessToken\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass AuthMapperTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldMap_NetworkAccessToken_to_LocalAccessToken() {\n        // given\n        val networkAccessToken = NetworkAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word(),\n            tokenType = \"bearer\",\n            scope = \"public\"\n        )\n\n        // when\n        val localAccessToken = networkAccessToken.toLocalAccessToken()\n\n        // then\n        assertThat(localAccessToken)\n            .usingRecursiveComparison()\n            .isEqualTo(networkAccessToken)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/mapper/CharacterMapperTest.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toCharacter\nimport io.github.drumber.kitsune.data.mapper.CharacterMapper.toLocalCharacter\nimport io.github.drumber.kitsune.testutils.localCharacter\nimport io.github.drumber.kitsune.testutils.networkCharacter\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass CharacterMapperTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldMap_NetworkCharacter_to_LocalCharacter() {\n        // given\n        val networkCharacter = networkCharacter(faker)\n\n        // when\n        val localCharacter = networkCharacter.toLocalCharacter()\n\n        // then\n        assertThat(localCharacter)\n            .usingRecursiveComparison()\n            .isEqualTo(networkCharacter)\n    }\n\n    @Test\n    fun shouldMap_NetworkCharacter_to_Character() {\n        // given\n        val networkCharacter = networkCharacter(faker)\n\n        // when\n        val character = networkCharacter.toCharacter()\n\n        // then\n        assertThat(character)\n            .usingRecursiveComparison()\n            .isEqualTo(networkCharacter)\n    }\n\n    @Test\n    fun shouldMap_LocalCharacter_to_Character() {\n        // given\n        val localCharacter = localCharacter(faker)\n\n        // when\n        val character = localCharacter.toCharacter()\n\n        // then\n        assertThat(character)\n            .usingRecursiveComparison()\n            .ignoringFields(\"mediaCharacters\")\n            .isEqualTo(localCharacter)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/mapper/MediaMapperTest.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toAnime\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toManga\nimport io.github.drumber.kitsune.data.mapper.MediaMapper.toMedia\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkMedia\nimport io.github.drumber.kitsune.testutils.networkAnime\nimport io.github.drumber.kitsune.testutils.networkManga\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass MediaMapperTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldMap_NetworkMedia_to_Media() {\n        // given\n        val networkAnime: NetworkMedia = networkAnime(faker)\n        val networkManga: NetworkMedia = networkManga(faker)\n\n        // when\n        val anime: Media = networkAnime.toMedia()\n        val manga: Media = networkManga.toMedia()\n\n        // then\n        assertThat(anime).isInstanceOf(Anime::class.java)\n        assertThat(manga).isInstanceOf(Manga::class.java)\n    }\n\n    @Test\n    fun shouldMap_NetworkAnime_to_Anime() {\n        // given\n        val networkAnime = networkAnime(faker)\n\n        // when\n        val anime = networkAnime.toAnime()\n\n        // then\n        assertThat(anime)\n            .usingRecursiveComparison()\n            .isEqualTo(networkAnime)\n    }\n\n    @Test\n    fun shouldMap_NetworkManga_to_Manga() {\n        // given\n        val networkManga = networkManga(faker)\n\n        // when\n        val manga = networkManga.toManga()\n\n        // then\n        assertThat(manga)\n            .usingRecursiveComparison()\n            .isEqualTo(networkManga)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/mapper/UserMapperTest.kt",
    "content": "package io.github.drumber.kitsune.data.mapper\n\nimport io.github.drumber.kitsune.data.mapper.UserMapper.toLocalUser\nimport io.github.drumber.kitsune.data.mapper.UserMapper.toUser\nimport io.github.drumber.kitsune.testutils.localUser\nimport io.github.drumber.kitsune.testutils.networkUser\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass UserMapperTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldMap_NetworkUser_to_LocalUser() {\n        // given\n        val networkUser = networkUser(faker)\n\n        // when\n        val localUser = networkUser.toLocalUser()\n\n        // then\n        assertThat(localUser)\n            .usingRecursiveComparison()\n            .isEqualTo(networkUser)\n    }\n\n    @Test\n    fun shouldMap_NetworkUser_to_User() {\n        // given\n        val networkUser = networkUser(faker)\n\n        // when\n        val user = networkUser.toUser()\n\n        // then\n        assertThat(user)\n            .usingRecursiveComparison()\n            .isEqualTo(networkUser)\n    }\n\n    @Test\n    fun shouldMap_LocalUser_to_NetworkUser() {\n        // given\n        val localUser = localUser(faker)\n\n        // when\n        val user = localUser.toUser()\n\n        // then\n        assertThat(user)\n            .usingRecursiveComparison()\n            .ignoringFields(\"favorites\", \"profileLinks\", \"stats\", \"waifu.mediaCharacters\")\n            .isEqualTo(localUser)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryEntryModificationTest.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nimport io.github.drumber.kitsune.testutils.libraryEntryModification\nimport io.github.drumber.kitsune.testutils.localLibraryEntry\nimport io.github.drumber.kitsune.testutils.localLibraryEntryModification\nimport io.github.drumber.kitsune.testutils.libraryEntry\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass LibraryEntryModificationTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldApplyModificationsToLibraryEntry() {\n        // given\n        val libraryEntry = localLibraryEntry(faker)\n        val modification = localLibraryEntryModification(faker)\n            .copy(id = libraryEntry.id)\n\n        // when\n        val libraryEntryWithModifications = modification.applyToLibraryEntry(libraryEntry)\n\n        // then\n        assertThat(libraryEntryWithModifications)\n            .usingRecursiveComparison()\n            .comparingOnlyFields(\"id\", \"media\")\n            .isEqualTo(libraryEntry)\n        assertThat(libraryEntryWithModifications)\n            .usingRecursiveComparison()\n            .ignoringFields(\"id\", \"media\")\n            .isNotEqualTo(libraryEntry)\n    }\n\n    @Test\n    fun shouldApplyModificationsToLibraryEntryIgnoringBlankNotes() {\n        // given\n        val libraryEntry = localLibraryEntry(faker).copy(notes = null)\n        val modification = localLibraryEntryModification(faker)\n            .copy(id = libraryEntry.id, notes = \"\")\n\n        // when\n        val libraryEntryWithModifications = modification.applyToLibraryEntry(libraryEntry)\n\n        // then\n        assertThat(libraryEntryWithModifications.notes).isNull()\n    }\n\n    @Test\n    fun shouldCreateLibraryEntryFromModification() {\n        // given\n        val modification = libraryEntryModification(faker)\n\n        // when\n        val libraryEntry = modification.toLocalLibraryEntry()\n\n        // then\n        assertThat(libraryEntry)\n            .usingRecursiveComparison()\n            .ignoringFields(\"progressedAt\", \"reactionSkipped\", \"media\", \"updatedAt\", \"reconsuming\")\n            .isEqualTo(modification)\n    }\n\n    @Test\n    fun shouldBeEqualToLibraryEntry() {\n        // given\n        val libraryEntry = libraryEntry(faker)\n        val modification = LibraryEntryModification\n            .withIdAndNulls(faker.internet().uuid())\n            .copy(\n                progress = libraryEntry.progress,\n                status = libraryEntry.status,\n                ratingTwenty = libraryEntry.ratingTwenty\n            )\n\n        // when\n        val isEqual = modification.isEqualToLibraryEntry(libraryEntry)\n\n        // then\n        assertThat(isEqual).isTrue\n    }\n\n    @Test\n    fun shouldNotBeEqualToLibraryEntry() {\n        // given\n        val libraryEntry = libraryEntry(faker)\n        val modification = LibraryEntryModification\n            .withIdAndNulls(faker.internet().uuid())\n            .copy(\n                progress = libraryEntry.progress?.plus(1)\n            )\n\n        // when\n        val isEqual = modification.isEqualToLibraryEntry(libraryEntry)\n\n        // then\n        assertThat(isEqual).isFalse\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/presentation/model/library/LibraryEntryWithModificationTest.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.library\n\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryModificationState.SYNCHRONIZING\nimport io.github.drumber.kitsune.testutils.libraryEntryModification\nimport io.github.drumber.kitsune.testutils.libraryEntry\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass LibraryEntryWithModificationTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldIsNotSyncedReturnTrue() {\n        // given\n        val wrapper = LibraryEntryWithModification(\n            libraryEntry(faker),\n            libraryEntryModification(faker)\n        )\n\n        // when\n        val isNotSynced = wrapper.isNotSynced\n\n        // then\n        assertThat(isNotSynced).isTrue\n    }\n\n    @Test\n    fun shouldIsNotSyncedReturnFalseWhenEqual() {\n        // given\n        val libraryEntry = libraryEntry(faker)\n        val wrapper = LibraryEntryWithModification(\n            libraryEntry,\n            LibraryEntryModification\n                .withIdAndNulls(libraryEntry.id)\n                .copy(progress = libraryEntry.progress)\n        )\n\n        // when\n        val isNotSynced = wrapper.isNotSynced\n\n        // then\n        assertThat(isNotSynced).isFalse\n    }\n\n    @Test\n    fun shouldIsNotSyncedReturnFalseWhenSynchronizing() {\n        // given\n        val wrapper = LibraryEntryWithModification(\n            libraryEntry(faker),\n            libraryEntryModification(faker).copy(state = SYNCHRONIZING)\n        )\n\n        // when\n        val isNotSynced = wrapper.isNotSynced\n\n        // then\n        assertThat(isNotSynced).isFalse\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/presentation/model/media/MediaTest.kt",
    "content": "package io.github.drumber.kitsune.data.presentation.model.media\n\nimport io.github.drumber.kitsune.R\nimport io.github.drumber.kitsune.testutils.anime\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass MediaTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldGetCorrectSeasonString() {\n        // given\n        val anime = anime(faker)\n\n        // when & then\n        mapOf(\n            \"2023-12-01\" to R.string.season_winter,\n            \"2024-02-29\" to R.string.season_winter,\n            \"2024-03-01\" to R.string.season_spring,\n            \"2024-05-31\" to R.string.season_spring,\n            \"2024-06-01\" to R.string.season_summer,\n            \"2024-08-31\" to R.string.season_summer,\n            \"2024-09-01\" to R.string.season_fall,\n            \"2024-11-30\" to R.string.season_fall,\n        ).forEach { (startDate, seasonStringRes) ->\n            val stringRes = anime.copy(startDate = startDate).seasonStringRes\n            assertThat(stringRes).`as`(\"Date $startDate\").isEqualTo(seasonStringRes)\n        }\n    }\n\n    @Test\n    fun shouldGetCorrectSeasonYear() {\n        // given\n        val anime = anime(faker)\n\n        // when & then\n        mapOf(\n            \"2023-12-01\" to \"2024\",\n            \"2024-02-29\" to \"2024\",\n            \"2024-11-30\" to \"2024\",\n        ).forEach { (startDate, seasonYear) ->\n            val year = anime.copy(startDate = startDate).seasonYear\n            assertThat(year).`as`(\"Date $startDate\").isEqualTo(seasonYear)\n        }\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/repository/AccessTokenRepositoryTest.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository.AccessTokenState\nimport io.github.drumber.kitsune.data.source.local.auth.AccessTokenLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.AccessTokenNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.auth.model.NetworkAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.ObtainAccessToken\nimport io.github.drumber.kitsune.data.source.network.auth.model.RefreshAccessToken\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport io.github.drumber.kitsune.testutils.useMockedAndroidLogger\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.test.runTest\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.argumentCaptor\nimport org.mockito.kotlin.doAnswer\nimport org.mockito.kotlin.doNothing\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\n\nclass AccessTokenRepositoryTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldGetAccessToken() {\n        // given\n        val accessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word()\n        )\n\n        val localAccessTokenDataSource = mock<AccessTokenLocalDataSource> {\n            on { loadAccessToken() } doReturn accessToken\n        }\n\n        val accessTokenRepository = AccessTokenRepository(\n            localAccessTokenDataSource = localAccessTokenDataSource,\n            remoteAccessTokenDataSource = mock(stubOnly = true)\n        )\n\n        // when\n        val actualAccessToken = useMockedAndroidLogger {\n            accessTokenRepository.getAccessToken()\n        }\n\n        // then\n        verify(localAccessTokenDataSource).loadAccessToken()\n        assertThat(actualAccessToken).isEqualTo(accessToken)\n    }\n\n    @Test\n    fun shouldCacheAccessToken() {\n        // given\n        val accessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word()\n        )\n\n        val localAccessTokenDataSource = mock<AccessTokenLocalDataSource> {\n            on { loadAccessToken() } doReturn accessToken\n        }\n\n        val accessTokenRepository = AccessTokenRepository(\n            localAccessTokenDataSource = localAccessTokenDataSource,\n            remoteAccessTokenDataSource = mock(stubOnly = true)\n        )\n\n        // when\n        val (firstAccessToken, secondAccessToken) = useMockedAndroidLogger {\n            Pair(\n                accessTokenRepository.getAccessToken(),\n                accessTokenRepository.getAccessToken()\n            )\n        }\n\n        // then\n        verify(localAccessTokenDataSource, times(1)).loadAccessToken()\n        assertThat(firstAccessToken).isEqualTo(secondAccessToken)\n    }\n\n    @Test\n    fun shouldClearAccessToken() = runTest {\n        // given\n        val localAccessTokenDataSource = mock<AccessTokenLocalDataSource> {\n            doNothing().on { clearAccessToken() }\n        }\n\n        val accessTokenRepository = AccessTokenRepository(\n            localAccessTokenDataSource = localAccessTokenDataSource,\n            remoteAccessTokenDataSource = mock(stubOnly = true)\n        )\n\n        useMockedAndroidLogger {\n            // when\n            accessTokenRepository.clearAccessToken()\n\n            // then\n            verify(localAccessTokenDataSource).clearAccessToken()\n            assertThat(accessTokenRepository.getAccessToken()).isNull()\n            assertThat(accessTokenRepository.accessTokenState.value).isEqualTo(AccessTokenState.NOT_PRESENT)\n        }\n    }\n\n    @Test\n    fun shouldObtainAccessToken(): Unit = runBlocking {\n        // given\n        val username = faker.name().username()\n        val password = faker.lorem().word()\n        val accessToken = NetworkAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word(),\n            tokenType = \"bearer\",\n            scope = \"public\"\n        )\n\n        val localAccessTokenDataSource = FakeAccessTokenLocalDataSource()\n        val remoteAccessTokenDataSource = mock<AccessTokenNetworkDataSource> {\n            onSuspend { obtainAccessToken(any()) } doReturn accessToken\n        }\n\n        val accessTokenRepository = AccessTokenRepository(\n            localAccessTokenDataSource = localAccessTokenDataSource,\n            remoteAccessTokenDataSource = remoteAccessTokenDataSource\n        )\n\n        useMockedAndroidLogger {\n            // when\n            val actualAccessToken = accessTokenRepository.obtainAccessToken(username, password)\n\n            // then\n            val obtainAccessTokenCaptor = argumentCaptor<ObtainAccessToken>()\n            verify(remoteAccessTokenDataSource).obtainAccessToken(obtainAccessTokenCaptor.capture())\n            assertThat(obtainAccessTokenCaptor.firstValue.username).isEqualTo(username)\n            assertThat(obtainAccessTokenCaptor.firstValue.password).isEqualTo(password)\n\n            assertThat(localAccessTokenDataSource.accessToken)\n                .usingRecursiveComparison()\n                .isEqualTo(accessToken)\n\n            assertThat(actualAccessToken).usingRecursiveComparison().isEqualTo(accessToken)\n            assertThat(accessTokenRepository.getAccessToken()).isEqualTo(actualAccessToken)\n\n            assertThat(accessTokenRepository.accessTokenState.value).isEqualTo(AccessTokenState.PRESENT)\n        }\n    }\n\n    @Test\n    fun shouldRefreshAccessToken(): Unit = runBlocking {\n        // given\n        val localAccessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word()\n        )\n        val networkAccessToken = NetworkAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word(),\n            tokenType = \"bearer\",\n            scope = \"public\"\n        )\n\n        val localAccessTokenDataSource = mock<AccessTokenLocalDataSource> {\n            on { loadAccessToken() } doReturn localAccessToken\n            doNothing().on { storeAccessToken(any()) }\n        }\n        val remoteAccessTokenDataSource = mock<AccessTokenNetworkDataSource> {\n            onSuspend { refreshToken(any()) } doReturn networkAccessToken\n        }\n\n        val accessTokenRepository = AccessTokenRepository(\n            localAccessTokenDataSource = localAccessTokenDataSource,\n            remoteAccessTokenDataSource = remoteAccessTokenDataSource\n        )\n\n        useMockedAndroidLogger {\n            // when\n            val actualAccessToken = accessTokenRepository.refreshAccessToken()\n\n            // then\n            val refreshAccessTokenCaptor = argumentCaptor<RefreshAccessToken>()\n            verify(remoteAccessTokenDataSource).refreshToken(refreshAccessTokenCaptor.capture())\n            assertThat(refreshAccessTokenCaptor.firstValue.refreshToken).isEqualTo(localAccessToken.refreshToken)\n\n            val storeAccessTokenCaptor = argumentCaptor<LocalAccessToken>()\n            verify(localAccessTokenDataSource).storeAccessToken(storeAccessTokenCaptor.capture())\n            assertThat(storeAccessTokenCaptor.firstValue)\n                .usingRecursiveComparison()\n                .isEqualTo(networkAccessToken)\n\n            assertThat(actualAccessToken).usingRecursiveComparison().isEqualTo(networkAccessToken)\n            assertThat(accessTokenRepository.getAccessToken()).isEqualTo(actualAccessToken)\n        }\n    }\n\n    @Test\n    fun shouldRefreshAccessTokenOnlyOnce(): Unit = runBlocking {\n        // given\n        val localAccessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.internet().uuid()\n        )\n        val networkAccessToken = {\n            NetworkAccessToken(\n                accessToken = faker.lorem().word(),\n                createdAt = faker.number().randomNumber(),\n                expiresIn = faker.number().randomNumber(),\n                refreshToken = faker.internet().uuid(),\n                tokenType = \"bearer\",\n                scope = \"public\"\n            )\n        }\n\n        val localAccessTokenDataSource = FakeAccessTokenLocalDataSource(localAccessToken)\n        val remoteAccessTokenDataSource = mock<AccessTokenNetworkDataSource> {\n            onSuspend { refreshToken(any()) } doAnswer {\n                runBlocking {\n                    delay(10) // simulate network delay\n                    networkAccessToken()\n                }\n            }\n        }\n\n        val accessTokenRepository = AccessTokenRepository(\n            localAccessTokenDataSource = localAccessTokenDataSource,\n            remoteAccessTokenDataSource = remoteAccessTokenDataSource\n        )\n\n        useMockedAndroidLogger {\n            // when\n            val firstAccessToken = async { accessTokenRepository.refreshAccessToken() }\n            val secondAccessToken = async { accessTokenRepository.refreshAccessToken() }\n\n            // then\n            awaitAll(firstAccessToken, secondAccessToken)\n            verify(remoteAccessTokenDataSource, times(1)).refreshToken(any())\n            assertThat(firstAccessToken.await()).isEqualTo(localAccessTokenDataSource.loadAccessToken())\n            assertThat(firstAccessToken.await()).isEqualTo(secondAccessToken.await())\n        }\n    }\n\n    @Test\n    fun shouldAccessTokenStateEmitPresent() {\n        // given\n        val accessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word()\n        )\n\n        val localAccessTokenDataSource = FakeAccessTokenLocalDataSource(accessToken)\n        val accessTokenRepository = AccessTokenRepository(\n            localAccessTokenDataSource = localAccessTokenDataSource,\n            remoteAccessTokenDataSource = mock(stubOnly = true)\n        )\n\n        // when\n        val accessTokenState = useMockedAndroidLogger {\n            accessTokenRepository.accessTokenState.value\n        }\n\n        // then\n        assertThat(accessTokenState).isEqualTo(AccessTokenState.PRESENT)\n    }\n\n    @Test\n    fun shouldAccessTokenStateEmitNotPresent() {\n        // given\n        val localAccessTokenDataSource = FakeAccessTokenLocalDataSource()\n        val accessTokenRepository = AccessTokenRepository(\n            localAccessTokenDataSource = localAccessTokenDataSource,\n            remoteAccessTokenDataSource = mock(stubOnly = true)\n        )\n\n        // when\n        val accessTokenState = useMockedAndroidLogger {\n            accessTokenRepository.accessTokenState.value\n        }\n\n        // then\n        assertThat(accessTokenState).isEqualTo(AccessTokenState.NOT_PRESENT)\n    }\n\n\n    class FakeAccessTokenLocalDataSource(\n        var accessToken: LocalAccessToken? = null\n    ) : AccessTokenLocalDataSource {\n\n        override fun loadAccessToken(): LocalAccessToken? {\n            return accessToken\n        }\n\n        override fun storeAccessToken(accessToken: LocalAccessToken) {\n            this.accessToken = accessToken\n        }\n\n        override fun clearAccessToken() {\n            accessToken = null\n        }\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/repository/AlgoliaKeyRepositoryTest.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.source.network.algolia.AlgoliaKeyNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.algolia.model.NetworkAlgoliaKey\nimport io.github.drumber.kitsune.data.source.network.algolia.model.NetworkAlgoliaKeyCollection\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport kotlinx.coroutines.runBlocking\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\n\nclass AlgoliaKeyRepositoryTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldGetAllAlgoliaKeys(): Unit = runBlocking {\n        // given\n        val algoliaKeyCollection = NetworkAlgoliaKeyCollection(\n            users = null,\n            posts = null,\n            media = NetworkAlgoliaKey(\n                key = faker.internet().uuid(),\n                index = \"users\"\n            ),\n            groups = null,\n            characters = null\n        )\n\n        val remoteAlgoliaKeyDataSource = mock<AlgoliaKeyNetworkDataSource> {\n            onSuspend { getAllAlgoliaKeys() } doReturn algoliaKeyCollection\n        }\n\n        val algoliaKeyRepository = AlgoliaKeyRepository(remoteAlgoliaKeyDataSource)\n\n        // when\n        val actualAlgoliaKeyCollection = algoliaKeyRepository.getAllAlgoliaKeys()\n\n        // then\n        verify(remoteAlgoliaKeyDataSource).getAllAlgoliaKeys()\n        assertThat(actualAlgoliaKeyCollection)\n            .usingRecursiveComparison()\n            .isEqualTo(algoliaKeyCollection)\n    }\n\n    @Test\n    fun shouldGetCachedAlgoliaKeys(): Unit = runBlocking {\n        // given\n        val algoliaKeyCollection = NetworkAlgoliaKeyCollection(\n            users = null,\n            posts = null,\n            media = NetworkAlgoliaKey(\n                key = faker.internet().uuid(),\n                index = \"users\"\n            ),\n            groups = null,\n            characters = null\n        )\n\n        val remoteAlgoliaKeyDataSource = mock<AlgoliaKeyNetworkDataSource> {\n            onSuspend { getAllAlgoliaKeys() } doReturn algoliaKeyCollection\n        }\n\n        val algoliaKeyRepository = AlgoliaKeyRepository(remoteAlgoliaKeyDataSource)\n\n        // when\n        val algoliaKeyCollection1 = algoliaKeyRepository.getAllAlgoliaKeys()\n        val algoliaKeyCollection2 = algoliaKeyRepository.getAllAlgoliaKeys()\n\n        // then\n        verify(remoteAlgoliaKeyDataSource, times(1)).getAllAlgoliaKeys()\n        assertThat(algoliaKeyCollection1).isEqualTo(algoliaKeyCollection2)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/repository/AppUpdateRepositoryTest.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.presentation.model.appupdate.UpdateCheckResult\nimport io.github.drumber.kitsune.data.source.network.appupdate.AppReleaseNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.appupdate.model.NetworkGitHubRelease\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport kotlinx.coroutines.test.runTest\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.junit.runners.Parameterized\nimport org.junit.runners.Parameterized.Parameters\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\n\n@RunWith(Parameterized::class)\nclass AppUpdateRepositoryTest(\n    private val localVersion: String,\n    private val remoteVersion: String,\n    private val isNewVersion: Boolean\n) {\n\n    companion object {\n        @JvmStatic\n        @Parameters(name = \"{index}: localVersion={0}, remoteVersion={1}, isNewVersion={2}\")\n        fun data(): Collection<Array<Any>> {\n            return listOf(\n                // localVersion, remoteVersion, isNewVersion\n                arrayOf(\"1.0.0\", \"1.0.1\", true),\n                arrayOf(\"1.0.0\", \"v1.0.1\", true),\n                arrayOf(\"1.0.0\", \"v1.0.0\", false),\n                arrayOf(\"1.0\", \"v1.0.0\", false),\n                arrayOf(\"1.0.0\", \"v2.0\", true),\n                arrayOf(\"2.0\", \"v1.0.0\", false),\n                arrayOf(\"1.2.3\", \"v1.1.4\", false),\n                arrayOf(\"1.0.0\", \"v1.2.4\", true),\n                arrayOf(\"1.0.0\", \"v1.2.4\", true),\n                arrayOf(\"1.0.0\", \"v1.2.4-beta\", true),\n                arrayOf(\"1.0.0-beta1\", \"1.0.0\", true),\n                arrayOf(\"1.0-beta1\", \"1.0.0\", true),\n                arrayOf(\"1.0.0-beta2\", \"1.0.0-beta1\", false),\n                arrayOf(\"1.0.0-beta1\", \"1.0.0-beta2\", true),\n                arrayOf(\"1.0.0-beta2\", \"1.0.0-rc1\", true),\n                arrayOf(\"1.0\", \"1.0.0-beta1\", false),\n                arrayOf(\"1.0.1-beta1\", \"1.0.0\", false),\n                arrayOf(\"1.0.0-debug\", \"1.0.0\", false)\n            )\n        }\n    }\n\n    @Test\n    fun shouldCheckForUpdates() = runTest {\n        // given\n        val appReleaseDataSource = mock<AppReleaseNetworkDataSource> {\n            onSuspend { getLatestRelease() } doReturn NetworkGitHubRelease(\n                version = remoteVersion,\n                url = \"\",\n                publishDate = \"\"\n            )\n        }\n        val repository = AppUpdateRepository(appReleaseDataSource)\n\n        // when\n        val updateCheckResult = repository.checkForUpdates(localVersion)\n\n        // then\n        if (isNewVersion) {\n            assertThat(updateCheckResult).isInstanceOf(UpdateCheckResult.NewVersion::class.java)\n            assertThat((updateCheckResult as UpdateCheckResult.NewVersion).release.version)\n                .isEqualTo(remoteVersion)\n        } else {\n            assertThat(updateCheckResult).isInstanceOf(UpdateCheckResult.NoNewVersion::class.java)\n        }\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/repository/LibraryRepositoryTest.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.common.exception.NotFoundException\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLibraryEntry\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLocalLibraryEntry\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toLocalLibraryEntryModification\nimport io.github.drumber.kitsune.data.mapper.LibraryMapper.toNetworkLibraryStatus\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.source.local.library.LibraryLocalDataSource\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntry\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryModificationState.NOT_SYNCHRONIZED\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryModificationState.SYNCHRONIZING\nimport io.github.drumber.kitsune.data.source.network.library.LibraryNetworkDataSource\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryEntry\nimport io.github.drumber.kitsune.testutils.anime\nimport io.github.drumber.kitsune.testutils.assertThatThrownBy\nimport io.github.drumber.kitsune.testutils.localLibraryEntry\nimport io.github.drumber.kitsune.testutils.network.FakeHttpException\nimport io.github.drumber.kitsune.testutils.networkLibraryEntry\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport io.github.drumber.kitsune.testutils.useMockedAndroidLogger\nimport io.github.drumber.kitsune.util.DATE_FORMAT_ISO\nimport io.github.drumber.kitsune.util.formatDate\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.plus\nimport kotlinx.coroutines.test.runTest\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.argumentCaptor\nimport org.mockito.kotlin.doAnswer\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.eq\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\nimport java.io.IOException\nimport java.time.Instant\nimport java.util.Date\n\nclass LibraryRepositoryTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldAddNewLibraryEntry() = runTest {\n        // given\n        val userId = faker.internet().uuid()\n        val media = anime(faker)\n        val status = LibraryStatus.Planned\n        val libraryEntry = networkLibraryEntry(faker)\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { postLibraryEntry(any(), any()) } doReturn libraryEntry\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { insertLibraryEntry(any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope\n        )\n\n        // when\n        val newLibraryEntry = libraryRepository.addNewLibraryEntry(userId, media, status)\n\n        // then\n        val networkLibraryEntryArg = argumentCaptor<NetworkLibraryEntry>()\n        val localLibraryEntryArg = argumentCaptor<LocalLibraryEntry>()\n        verify(remoteDataSource).postLibraryEntry(networkLibraryEntryArg.capture(), any())\n        verify(localDataSource).insertLibraryEntry(localLibraryEntryArg.capture())\n\n        assertThat(networkLibraryEntryArg.firstValue.user?.id).isEqualTo(userId)\n        assertThat(networkLibraryEntryArg.firstValue.anime?.id).isEqualTo(media.id)\n        assertThat(networkLibraryEntryArg.firstValue.status).isEqualTo(status.toNetworkLibraryStatus())\n\n        assertThat(localLibraryEntryArg.firstValue.id).isEqualTo(libraryEntry.id)\n\n        assertThat(newLibraryEntry).isNotNull\n        assertThat(newLibraryEntry?.id).isEqualTo(libraryEntry.id)\n    }\n\n    @Test\n    fun shouldNotAddNewLibraryEntryOnNetworkError() = runTest {\n        // given\n        val userId = faker.internet().uuid()\n        val media = anime(faker)\n        val status = LibraryStatus.Planned\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { postLibraryEntry(any(), any()) } doAnswer { throw IOException() }\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { insertLibraryEntry(any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope + SupervisorJob()\n        )\n\n        // then\n        assertThatThrownBy {\n            // when\n            libraryRepository.addNewLibraryEntry(userId, media, status)\n        }.isInstanceOf(IOException::class.java)\n\n        verify(remoteDataSource).postLibraryEntry(any(), any())\n        verify(localDataSource, times(0)).insertLibraryEntry(any())\n    }\n\n\n    @Test\n    fun shouldRemoveLibraryEntry() = runTest {\n        // given\n        val libraryEntryId = faker.internet().uuid()\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { deleteLibraryEntry(any()) } doReturn Unit\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { deleteLibraryEntryAndAnyModification(any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope\n        )\n\n        // when\n        libraryRepository.removeLibraryEntry(libraryEntryId)\n\n        // then\n        verify(remoteDataSource).deleteLibraryEntry(libraryEntryId)\n        verify(localDataSource).deleteLibraryEntryAndAnyModification(libraryEntryId)\n    }\n\n    @Test\n    fun shouldNotRemoveLibraryEntry() = runTest {\n        // given\n        val libraryEntryId = faker.internet().uuid()\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { deleteLibraryEntry(any()) } doAnswer { throw IOException() }\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { deleteLibraryEntryAndAnyModification(any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope\n        )\n\n        // then\n        assertThatThrownBy {\n            // when\n            libraryRepository.removeLibraryEntry(libraryEntryId)\n        }.isInstanceOf(IOException::class.java)\n\n        verify(remoteDataSource).deleteLibraryEntry(libraryEntryId)\n        verify(localDataSource, never()).insertLibraryEntry(any())\n    }\n\n    @Test\n    fun shouldMayRemoveLibraryEntryLocally() = runTest {\n        // given\n        val libraryEntryId = faker.internet().uuid()\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { getLibraryEntry(any(), any()) } doAnswer { throw FakeHttpException(404) }\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { deleteLibraryEntryAndAnyModification(any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope\n        )\n\n        // when\n        libraryRepository.mayRemoveLibraryEntryLocally(libraryEntryId)\n\n        // then\n        verify(remoteDataSource).getLibraryEntry(eq(libraryEntryId), any())\n        verify(localDataSource).deleteLibraryEntryAndAnyModification(libraryEntryId)\n    }\n\n    @Test\n    fun shouldNotMayRemoveLibraryEntryLocally() = runTest {\n        // given\n        val libraryEntryId = faker.internet().uuid()\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { getLibraryEntry(any(), any()) } doReturn networkLibraryEntry(faker)\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { deleteLibraryEntryAndAnyModification(any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope\n        )\n\n        // when\n        libraryRepository.mayRemoveLibraryEntryLocally(libraryEntryId)\n\n        // then\n        verify(remoteDataSource).getLibraryEntry(eq(libraryEntryId), any())\n        verify(localDataSource, never()).deleteLibraryEntryAndAnyModification(any())\n    }\n\n    @Test\n    fun shouldUpdateLibraryEntry() = runTest {\n        // given\n        val libraryEntryModification = LibraryEntryModification\n            .withIdAndNulls(faker.internet().uuid())\n            .copy(status = LibraryStatus.Completed)\n        val expectedLibraryEntry = networkLibraryEntry(faker)\n            .copy(\n                id = libraryEntryModification.id,\n                status = libraryEntryModification.status?.toNetworkLibraryStatus()\n            )\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { updateLibraryEntry(any(), any(), any()) } doReturn expectedLibraryEntry\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { insertLibraryEntryModification(any()) } doReturn Unit\n            onSuspend { updateLibraryEntryAndDeleteModification(any(), any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope\n        )\n\n        // when\n        val result = libraryRepository.updateLibraryEntry(libraryEntryModification)\n\n        // then\n        verify(remoteDataSource).updateLibraryEntry(eq(libraryEntryModification.id), any(), any())\n        verify(localDataSource).updateLibraryEntryAndDeleteModification(\n            expectedLibraryEntry.toLocalLibraryEntry(),\n            libraryEntryModification.toLocalLibraryEntryModification().copy(state = SYNCHRONIZING)\n        )\n        verify(localDataSource).insertLibraryEntryModification(\n            libraryEntryModification.toLocalLibraryEntryModification().copy(state = SYNCHRONIZING)\n        )\n        assertThat(result).isEqualTo(expectedLibraryEntry.toLibraryEntry())\n    }\n\n    @Test\n    fun shouldNotOverwriteMoreRecentChangesOnUpdateLibraryEntry() = runTest {\n        // given\n        val now = Instant.now()\n        val libraryEntryModification = LibraryEntryModification\n            .withIdAndNulls(faker.internet().uuid())\n\n        val libraryEntryFromService = networkLibraryEntry(faker)\n            .copy(updatedAt = Date.from(now).formatDate(DATE_FORMAT_ISO))\n\n        val libraryEntryFromDb = localLibraryEntry(faker)\n            .copy(updatedAt = Date.from(now.plusMillis(1)).formatDate(DATE_FORMAT_ISO))\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { updateLibraryEntry(any(), any(), any()) } doReturn libraryEntryFromService\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { insertLibraryEntryModification(any()) } doReturn Unit\n            onSuspend { updateLibraryEntryAndDeleteModification(any(), any()) } doReturn Unit\n            onSuspend { getLibraryEntry(any()) } doReturn libraryEntryFromDb\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope\n        )\n\n        // when\n        libraryRepository.updateLibraryEntry(libraryEntryModification)\n\n        // then\n        verify(localDataSource, never()).updateLibraryEntryAndDeleteModification(any(), any())\n    }\n\n    @Test\n    fun shouldFailOnUpdateLibraryEntry() = runTest {\n        // given\n        val libraryEntryModification = LibraryEntryModification\n            .withIdAndNulls(faker.internet().uuid())\n            .copy(status = LibraryStatus.Completed)\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend { updateLibraryEntry(any(), any(), any()) } doAnswer { throw IOException() }\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { insertLibraryEntryModification(any()) } doReturn Unit\n            onSuspend { updateLibraryEntryAndDeleteModification(any(), any()) } doReturn Unit\n            onSuspend { deleteLibraryEntryAndAnyModification(any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope + SupervisorJob()\n        )\n\n        // then\n        assertThatThrownBy {\n            // when\n            useMockedAndroidLogger {\n                libraryRepository.updateLibraryEntry(libraryEntryModification)\n            }\n        }\n\n        verify(remoteDataSource).updateLibraryEntry(eq(libraryEntryModification.id), any(), any())\n        verify(localDataSource).insertLibraryEntryModification(\n            libraryEntryModification.toLocalLibraryEntryModification()\n                .copy(state = NOT_SYNCHRONIZED)\n        )\n        verify(localDataSource, never()).updateLibraryEntryAndDeleteModification(any(), any())\n        verify(localDataSource, never()).deleteLibraryEntryAndAnyModification(any())\n    }\n\n    @Test\n    fun shouldDeleteOnUpdateLibraryEntryWithNotFoundResponse() = runTest {\n        // given\n        val libraryEntryModification = LibraryEntryModification\n            .withIdAndNulls(faker.internet().uuid())\n            .copy(status = LibraryStatus.Completed)\n\n        val remoteDataSource = mock<LibraryNetworkDataSource> {\n            onSuspend {\n                updateLibraryEntry(\n                    any(),\n                    any(),\n                    any()\n                )\n            } doAnswer { throw FakeHttpException(404) }\n        }\n        val localDataSource = mock<LibraryLocalDataSource> {\n            onSuspend { insertLibraryEntryModification(any()) } doReturn Unit\n            onSuspend { updateLibraryEntryAndDeleteModification(any(), any()) } doReturn Unit\n            onSuspend { deleteLibraryEntryAndAnyModification(any()) } doReturn Unit\n        }\n\n        val libraryRepository = LibraryRepository(\n            remoteDataSource,\n            localDataSource,\n            NoOpLibraryChangeListener,\n            backgroundScope + SupervisorJob()\n        )\n\n        // then\n        assertThatThrownBy {\n            // when\n            libraryRepository.updateLibraryEntry(libraryEntryModification)\n        }.isInstanceOf(NotFoundException::class.java)\n\n        verify(remoteDataSource).updateLibraryEntry(eq(libraryEntryModification.id), any(), any())\n        verify(localDataSource).insertLibraryEntryModification(\n            libraryEntryModification.toLocalLibraryEntryModification()\n                .copy(state = SYNCHRONIZING)\n        )\n        verify(localDataSource, never()).updateLibraryEntryAndDeleteModification(any(), any())\n        verify(localDataSource).deleteLibraryEntryAndAnyModification(libraryEntryModification.id)\n    }\n\n\n    object NoOpLibraryChangeListener : LibraryChangeListener {\n        override fun onNewLibraryEntry(libraryEntry: LibraryEntry) {}\n\n        override fun onUpdateLibraryEntry(\n            libraryEntryModification: LibraryEntryModification,\n            updatedLibraryEntry: LibraryEntry?\n        ) {\n        }\n\n        override fun onRemoveLibraryEntry(id: String) {}\n\n        override fun onDataInsertion(libraryEntries: List<LibraryEntry>) {}\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/repository/UserRepositoryTest.kt",
    "content": "package io.github.drumber.kitsune.data.repository\n\nimport io.github.drumber.kitsune.data.source.local.user.UserLocalDataSource\nimport io.github.drumber.kitsune.data.source.network.user.UserNetworkDataSource\nimport io.github.drumber.kitsune.testutils.localUser\nimport io.github.drumber.kitsune.testutils.networkUser\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport kotlinx.coroutines.flow.toList\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.test.UnconfinedTestDispatcher\nimport kotlinx.coroutines.test.runTest\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doNothing\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\n\nclass UserRepositoryTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldFetchAndStoreLocalUserFromNetwork() = runTest {\n        // given\n        val user = networkUser(faker)\n\n        val localUserDataSource = mock<UserLocalDataSource> {\n            doNothing().on { storeUser(any()) }\n        }\n\n        val remoteUserDataSource = mock<UserNetworkDataSource> {\n            onSuspend { getSelf(any()) } doReturn user\n        }\n\n        val userRepository = UserRepository(localUserDataSource, remoteUserDataSource, backgroundScope)\n\n        // when\n        userRepository.fetchAndStoreLocalUserFromNetwork()\n\n        // then\n        verify(remoteUserDataSource).getSelf(any())\n        verify(localUserDataSource).storeUser(any())\n        assertThat(userRepository.localUser.value).isNotNull\n    }\n\n    @Test\n    fun shouldStoreLocalUser() = runTest {\n        // given\n        val user = localUser(faker)\n\n        val localUserDataSource = mock<UserLocalDataSource> {\n            doNothing().on { storeUser(any()) }\n        }\n\n        val remoteUserDataSource = mock<UserNetworkDataSource>()\n\n        val userRepository = UserRepository(localUserDataSource, remoteUserDataSource, backgroundScope)\n\n        // when\n        userRepository.storeLocalUser(user)\n\n        // then\n        verify(localUserDataSource).storeUser(any())\n        assertThat(userRepository.localUser.value).isEqualTo(user)\n    }\n\n    @Test\n    fun shouldExposeLocalUserAsStateFlow() = runTest {\n        // given\n        val networkUser = networkUser(faker)\n        var user = localUser(faker)\n\n        val localUserDataSource = mock<UserLocalDataSource> {\n            doNothing().on { storeUser(any()) }\n            on { loadUser() } doReturn user\n        }\n\n        val remoteUserDataSource = mock<UserNetworkDataSource> {\n            onSuspend { getSelf(any()) } doReturn networkUser\n        }\n\n        val userRepository = UserRepository(localUserDataSource, remoteUserDataSource, backgroundScope)\n\n        // when & then\n        verify(localUserDataSource).loadUser()\n        assertThat(userRepository.localUser.value).isEqualTo(user)\n        assertThat(userRepository.hasLocalUser()).isTrue()\n\n        user = user.copy(id = \"2\")\n        userRepository.storeLocalUser(user)\n        assertThat(userRepository.localUser.value).isEqualTo(user)\n\n        userRepository.clearLocalUser()\n        assertThat(userRepository.localUser.value).isNull()\n        assertThat(userRepository.hasLocalUser()).isFalse()\n\n        userRepository.fetchAndStoreLocalUserFromNetwork()\n        assertThat(userRepository.localUser.value?.id).isEqualTo(networkUser.id)\n        assertThat(userRepository.hasLocalUser()).isTrue()\n    }\n\n    @Test\n    fun shouldTriggerReLogInPrompt() = runTest {\n        // given\n        val localUserDataSource = mock<UserLocalDataSource>()\n        val remoteUserDataSource = mock<UserNetworkDataSource>()\n\n        val userRepository = UserRepository(localUserDataSource, remoteUserDataSource, backgroundScope)\n\n        // when\n        val results = mutableListOf<Unit>()\n        backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {\n            userRepository.userReLogInPrompt.toList(results)\n        }\n\n        userRepository.promptUserReLogIn()\n        userRepository.promptUserReLogIn()\n\n        // then\n        assertThat(results).hasSize(2)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/source/local/auth/LocalAccessTokenTest.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.auth\n\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass LocalAccessTokenTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldGetExpirationTimeInSeconds() {\n        // given\n        val localAccessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            refreshToken = faker.lorem().word(),\n            createdAt = 1000,\n            expiresIn = 5000\n        )\n\n        // when\n        val expirationTimeInSeconds = localAccessToken.getExpirationTimeInSeconds()\n\n        // then\n        assertThat(expirationTimeInSeconds).isEqualTo(6000)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/source/local/library/model/LocalLibraryEntryModificationTest.kt",
    "content": "package io.github.drumber.kitsune.data.source.local.library.model\n\nimport io.github.drumber.kitsune.testutils.localLibraryEntry\nimport io.github.drumber.kitsune.util.DATE_FORMAT_ISO\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.assertj.core.api.Assertions.assertThatThrownBy\nimport org.junit.Test\n\nclass LocalLibraryEntryModificationTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldBeEqualToLibraryEntry() {\n        // given\n        val libraryEntry = localLibraryEntry(faker)\n\n        val testCases = listOf(\n            LocalLibraryEntryModification(\n                id = libraryEntry.id,\n                startedAt = libraryEntry.startedAt,\n                finishedAt = libraryEntry.finishedAt,\n                status = libraryEntry.status,\n                progress = libraryEntry.progress,\n                reconsumeCount = libraryEntry.reconsumeCount,\n                volumesOwned = libraryEntry.volumesOwned,\n                ratingTwenty = libraryEntry.ratingTwenty,\n                notes = libraryEntry.notes,\n                privateEntry = libraryEntry.privateEntry\n            ),\n            LocalLibraryEntryModification(\n                id = libraryEntry.id,\n                startedAt = null,\n                finishedAt = null,\n                status = null,\n                progress = null,\n                reconsumeCount = null,\n                volumesOwned = null,\n                ratingTwenty = null,\n                notes = null,\n                privateEntry = null\n            ),\n            LocalLibraryEntryModification(\n                id = libraryEntry.id,\n                startedAt = null,\n                finishedAt = null,\n                status = null,\n                progress = libraryEntry.progress,\n                reconsumeCount = null,\n                volumesOwned = null,\n                ratingTwenty = null,\n                notes = null,\n                privateEntry = null\n            ),\n            LocalLibraryEntryModification(\n                id = libraryEntry.id,\n                startedAt = null,\n                finishedAt = null,\n                status = null,\n                progress = null,\n                reconsumeCount = null,\n                volumesOwned = null,\n                ratingTwenty = null,\n                notes = libraryEntry.notes,\n                privateEntry = null\n            )\n        )\n\n        // when & then\n        testCases.forEach { libraryEntryModification ->\n            val isEqual = libraryEntryModification.isEqualToLibraryEntry(libraryEntry)\n            assertThat(isEqual).`as`(\"Modification $libraryEntryModification\").isTrue()\n        }\n    }\n\n    @Test\n    fun shouldNotBeEqualToLibraryEntry() {\n        // given\n        val libraryEntry = localLibraryEntry(faker)\n\n        val testCases = listOf(\n            LocalLibraryEntryModification(\n                id = libraryEntry.id,\n                startedAt = libraryEntry.startedAt,\n                finishedAt = libraryEntry.finishedAt,\n                status = libraryEntry.status,\n                progress = (libraryEntry.progress ?: 0) + 1,\n                reconsumeCount = libraryEntry.reconsumeCount,\n                volumesOwned = libraryEntry.volumesOwned,\n                ratingTwenty = libraryEntry.ratingTwenty,\n                notes = libraryEntry.notes,\n                privateEntry = libraryEntry.privateEntry\n            ),\n            LocalLibraryEntryModification(\n                id = libraryEntry.id,\n                startedAt = null,\n                finishedAt = null,\n                status = null,\n                progress = null,\n                reconsumeCount = null,\n                volumesOwned = null,\n                ratingTwenty = null,\n                notes = null,\n                privateEntry = libraryEntry.privateEntry?.not() ?: true\n            ),\n            LocalLibraryEntryModification(\n                id = \"foo\",\n                startedAt = null,\n                finishedAt = null,\n                status = null,\n                progress = null,\n                reconsumeCount = null,\n                volumesOwned = null,\n                ratingTwenty = null,\n                notes = null,\n                privateEntry = null\n            )\n        )\n\n        // when & then\n        testCases.forEach { libraryEntryModification ->\n            val isEqual = libraryEntryModification.isEqualToLibraryEntry(libraryEntry)\n            assertThat(isEqual).`as`(\"Modification $libraryEntryModification\").isFalse()\n        }\n    }\n\n    @Test\n    fun shouldApplyToLibraryEntry() {\n        // given\n        val libraryEntry = localLibraryEntry(faker)\n\n        val modification = LocalLibraryEntryModification(\n            id = libraryEntry.id,\n            startedAt = faker.date().birthday(DATE_FORMAT_ISO),\n            finishedAt = faker.date().birthday(DATE_FORMAT_ISO),\n            status = LocalLibraryStatus.Completed,\n            progress = 999,\n            reconsumeCount = 123,\n            volumesOwned = 456,\n            ratingTwenty = 19,\n            notes = \"foo\",\n            privateEntry = true\n        )\n\n        // when\n        val actualLibraryEntry = modification.applyToLibraryEntry(libraryEntry)\n\n        // then\n        assertThat(actualLibraryEntry)\n            .usingRecursiveComparison()\n            .ignoringFields(\"media\", \"progressedAt\", \"reactionSkipped\", \"updatedAt\", \"reconsuming\")\n            .isEqualTo(modification)\n    }\n\n    @Test\n    fun shouldNotApplyBlankNotesToLibraryEntry() {\n        // given\n        val libraryEntry = localLibraryEntry(faker).copy(notes = null)\n\n        val modification = LocalLibraryEntryModification(\n            id = libraryEntry.id,\n            startedAt = null,\n            finishedAt = null,\n            status = null,\n            progress = null,\n            reconsumeCount = null,\n            volumesOwned = null,\n            ratingTwenty = null,\n            notes = \"\",\n            privateEntry = null\n        )\n\n        // when\n        val actualLibraryEntry = modification.applyToLibraryEntry(libraryEntry)\n\n        // then\n        assertThat(actualLibraryEntry.notes).isNull()\n    }\n\n    @Test\n    fun shouldApplyBlankNotesToLibraryEntryIfItHasNotes() {\n        // given\n        val libraryEntry = localLibraryEntry(faker).copy(notes = \"foo\")\n\n        val modification = LocalLibraryEntryModification(\n            id = libraryEntry.id,\n            startedAt = null,\n            finishedAt = null,\n            status = null,\n            progress = null,\n            reconsumeCount = null,\n            volumesOwned = null,\n            ratingTwenty = null,\n            notes = \"\",\n            privateEntry = null\n        )\n\n        // when\n        val actualLibraryEntry = modification.applyToLibraryEntry(libraryEntry)\n\n        // then\n        assertThat(actualLibraryEntry.notes).isEqualTo(\"\")\n    }\n\n    @Test\n    fun shouldThrowIfApplyToLibraryEntryWithDifferentId() {\n        // given\n        val libraryEntry = localLibraryEntry(faker)\n\n        val modification = LocalLibraryEntryModification(\n            id = \"foo\",\n            startedAt = faker.date().birthday(DATE_FORMAT_ISO),\n            finishedAt = faker.date().birthday(DATE_FORMAT_ISO),\n            status = LocalLibraryStatus.Completed,\n            progress = 999,\n            reconsumeCount = 123,\n            volumesOwned = 456,\n            ratingTwenty = 19,\n            notes = \"foo\",\n            privateEntry = true\n        )\n\n        // when\n        assertThatThrownBy {\n            // when\n            modification.applyToLibraryEntry(libraryEntry)\n        }.isInstanceOf(Exception::class.java)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/data/source/network/PageDataTest.kt",
    "content": "package io.github.drumber.kitsune.data.source.network\n\nimport com.github.jasminb.jsonapi.JSONAPIDocument\nimport com.github.jasminb.jsonapi.JSONAPISpecConstants\nimport com.github.jasminb.jsonapi.Link\nimport com.github.jasminb.jsonapi.Links\nimport io.github.drumber.kitsune.testutils.networkAnime\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.RobolectricTestRunner\n\n@RunWith(RobolectricTestRunner::class)\nclass PageDataTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldMap_JSONAPIDocument_to_PageData() {\n        // given\n        val data = List(5) { networkAnime(faker) }\n\n        val links = mutableMapOf(\n            JSONAPISpecConstants.FIRST to Link(\"https://example.com/api/edge/anime?page%5Blimit%5D=5&page%5Boffset%5D=0\"),\n            JSONAPISpecConstants.LAST to Link(\"https://example.com/api/edge/anime?page%5Blimit%5D=5&page%5Boffset%5D=20541\"),\n            JSONAPISpecConstants.NEXT to Link(\"https://example.com/api/edge/anime?page%5Blimit%5D=5&page%5Boffset%5D=5\")\n        )\n\n        val jsonApiDocument = JSONAPIDocument(data, Links(links), emptyMap())\n\n        // when\n        val pageData = jsonApiDocument.toPageData()\n\n        // then\n        assertThat(pageData.data).isEqualTo(data)\n        assertThat(pageData.first).isEqualTo(0)\n        assertThat(pageData.last).isEqualTo(20541)\n        assertThat(pageData.next).isEqualTo(5)\n        assertThat(pageData.prev).isNull()\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/domain/auth/LogInUserUseCaseTest.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.testutils.network.FakeHttpException\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport io.github.drumber.kitsune.testutils.useMockedAndroidLogger\nimport kotlinx.coroutines.runBlocking\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.mockito.kotlin.doAnswer\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.doThrow\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\nimport java.net.UnknownHostException\n\nclass LogInUserUseCaseTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldLogInWithSuccess(): Unit = runBlocking {\n        // given\n        val username = faker.internet().emailAddress()\n        val password = faker.internet().password()\n        val accessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word()\n        )\n\n        val userRepository = mock<UserRepository> {\n            onSuspend { fetchAndStoreLocalUserFromNetwork() } doReturn Unit\n        }\n\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            on { hasAccessToken() } doReturn false\n            onSuspend { obtainAccessToken(username, password) } doReturn accessToken\n        }\n\n        val isUserLoggedInUserCase = mock<IsUserLoggedInUseCase> {\n            on { invoke() } doReturn false\n        }\n\n        val logInUser = LogInUserUseCase(\n            userRepository,\n            accessTokenRepository,\n            isUserLoggedInUserCase\n        )\n\n        // when\n        val result = useMockedAndroidLogger { logInUser(username, password) }\n\n        // then\n        verify(accessTokenRepository).obtainAccessToken(username, password)\n        verify(userRepository).fetchAndStoreLocalUserFromNetwork()\n        verify(isUserLoggedInUserCase).invoke()\n        assertThat(result).isEqualTo(LoginResult.Success(accessToken, null))\n    }\n\n    @Test\n    fun shouldLogInWithFailure(): Unit = runBlocking {\n        // given\n        val username = faker.internet().emailAddress()\n        val password = faker.internet().password()\n\n        val userRepository = mock<UserRepository> {\n            onSuspend { fetchAndStoreLocalUserFromNetwork() } doReturn Unit\n        }\n\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            on { hasAccessToken() } doReturn false\n            onSuspend { obtainAccessToken(username, password) } doThrow FakeHttpException(400)\n        }\n\n        val isUserLoggedInUserCase = mock<IsUserLoggedInUseCase> {\n            on { invoke() } doReturn false\n        }\n\n        val logInUser = LogInUserUseCase(\n            userRepository,\n            accessTokenRepository,\n            isUserLoggedInUserCase\n        )\n\n        // when\n        val result = useMockedAndroidLogger { logInUser(username, password) }\n\n        // then\n        verify(accessTokenRepository).obtainAccessToken(username, password)\n        verify(userRepository, times(0)).fetchAndStoreLocalUserFromNetwork()\n        verify(isUserLoggedInUserCase).invoke()\n        assertThat(result).isEqualTo(LoginResult.Failure)\n    }\n\n    @Test\n    fun shouldLogInWithError(): Unit = runBlocking {\n        // given\n        val username = faker.internet().emailAddress()\n        val password = faker.internet().password()\n\n        val userRepository = mock<UserRepository> {\n            onSuspend { fetchAndStoreLocalUserFromNetwork() } doReturn Unit\n        }\n\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            on { hasAccessToken() } doReturn false\n            onSuspend {\n                obtainAccessToken(\n                    username,\n                    password\n                )\n            } doAnswer { throw UnknownHostException() }\n        }\n\n        val isUserLoggedInUserCase = mock<IsUserLoggedInUseCase> {\n            on { invoke() } doReturn false\n        }\n\n        val logInUser = LogInUserUseCase(\n            userRepository,\n            accessTokenRepository,\n            isUserLoggedInUserCase\n        )\n\n        // when\n        val result = useMockedAndroidLogger { logInUser(username, password) }\n\n        // then\n        verify(accessTokenRepository).obtainAccessToken(username, password)\n        verify(userRepository, times(0)).fetchAndStoreLocalUserFromNetwork()\n        verify(isUserLoggedInUserCase).invoke()\n        assertThat(result).isInstanceOf(LoginResult.Error::class.java)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/domain/auth/LogOutUserUseCaseTest.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport io.github.drumber.kitsune.testutils.useMockedAndroidLogger\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Test\nimport org.mockito.kotlin.doNothing\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\n\nclass LogOutUserUseCaseTest {\n\n    @Test\n    fun shouldLogOut() = runTest {\n        // given\n        val userRepository = mock<UserRepository> {\n            doNothing().on { clearLocalUser() }\n        }\n\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            onSuspend { clearAccessToken() } doReturn Unit\n        }\n\n        val logOutUser = LogOutUserUseCase(userRepository, accessTokenRepository)\n\n        // when\n        useMockedAndroidLogger { logOutUser() }\n\n        // then\n        verify(userRepository).clearLocalUser()\n        verify(accessTokenRepository).clearAccessToken()\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/domain/auth/RefreshAccessTokenIfExpiredUseCaseTest.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport io.github.drumber.kitsune.testutils.useMockedAndroidLogger\nimport kotlinx.coroutines.runBlocking\nimport net.datafaker.Faker\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass RefreshAccessTokenIfExpiredUseCaseTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldRefreshAccessTokenIfExpired(): Unit = runBlocking {\n        // given\n        val localAccessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = (System.currentTimeMillis().milliseconds - 1.days).inWholeSeconds,\n            expiresIn = 1.days.inWholeSeconds,\n            refreshToken = faker.lorem().word()\n        )\n\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            on { getAccessToken() } doReturn localAccessToken\n            onSuspend { refreshAccessToken() } doReturn localAccessToken\n        }\n\n        val refreshAccessTokenUseCase = mock<RefreshAccessTokenUseCase> {\n            onSuspend { invoke() } doReturn RefreshResult.Success(localAccessToken)\n        }\n\n        val refreshAccessTokenIfExpired =\n            RefreshAccessTokenIfExpiredUseCase(accessTokenRepository, refreshAccessTokenUseCase)\n\n        // when\n        useMockedAndroidLogger { refreshAccessTokenIfExpired() }\n\n        // then\n        verify(accessTokenRepository).getAccessToken()\n        verify(refreshAccessTokenUseCase).invoke()\n    }\n\n    @Test\n    fun shouldNotRefreshAccessTokenIfNotExpired(): Unit = runBlocking {\n        // given\n        val localAccessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = (System.currentTimeMillis().milliseconds - 1.days).inWholeSeconds,\n            expiresIn = 10.days.inWholeSeconds,\n            refreshToken = faker.lorem().word()\n        )\n\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            on { getAccessToken() } doReturn localAccessToken\n            onSuspend { refreshAccessToken() } doReturn localAccessToken\n        }\n\n        val refreshAccessTokenUseCase = mock<RefreshAccessTokenUseCase> {\n            onSuspend { invoke() } doReturn RefreshResult.Success(localAccessToken)\n        }\n\n        val refreshAccessTokenIfExpired =\n            RefreshAccessTokenIfExpiredUseCase(accessTokenRepository, refreshAccessTokenUseCase)\n\n        // when\n        useMockedAndroidLogger { refreshAccessTokenIfExpired() }\n\n        // then\n        verify(accessTokenRepository).getAccessToken()\n        verify(refreshAccessTokenUseCase, times(0)).invoke()\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/domain/auth/RefreshAccessTokenUseCaseTest.kt",
    "content": "package io.github.drumber.kitsune.domain.auth\n\nimport io.github.drumber.kitsune.data.repository.AccessTokenRepository\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.data.source.local.auth.model.LocalAccessToken\nimport io.github.drumber.kitsune.testutils.network.FakeHttpException\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport io.github.drumber.kitsune.testutils.useMockedAndroidLogger\nimport kotlinx.coroutines.runBlocking\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.mockito.kotlin.doAnswer\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.doThrow\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\nimport java.net.UnknownHostException\n\nclass RefreshAccessTokenUseCaseTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldRefreshAccessTokenWithSuccess(): Unit = runBlocking {\n        // given\n        val accessToken = LocalAccessToken(\n            accessToken = faker.lorem().word(),\n            createdAt = faker.number().randomNumber(),\n            expiresIn = faker.number().randomNumber(),\n            refreshToken = faker.lorem().word()\n        )\n\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            onSuspend { refreshAccessToken() } doReturn accessToken\n        }\n\n        val userRepository = mock<UserRepository> {\n            onSuspend { promptUserReLogIn() } doReturn Unit\n        }\n\n        val logOutUserUseCase = mock<LogOutUserUseCase> {\n            onSuspend { invoke() } doReturn Unit\n        }\n\n        val refreshAccessToken = RefreshAccessTokenUseCase(accessTokenRepository, userRepository, logOutUserUseCase)\n\n        // when\n        val result = useMockedAndroidLogger { refreshAccessToken() }\n\n        // then\n        verify(accessTokenRepository).refreshAccessToken()\n        verify(logOutUserUseCase, times(0)).invoke()\n        assertThat(result).isEqualTo(RefreshResult.Success(accessToken))\n    }\n\n    @Test\n    fun shouldRefreshAccessTokenWithFailure(): Unit = runBlocking {\n        // given\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            onSuspend { refreshAccessToken() } doThrow FakeHttpException(400)\n            onSuspend { clearAccessToken() } doReturn Unit\n        }\n\n        val userRepository = mock<UserRepository> {\n            onSuspend { promptUserReLogIn() } doReturn Unit\n        }\n\n        val logOutUserUseCase = mock<LogOutUserUseCase> {\n            onSuspend { invoke() } doReturn Unit\n        }\n\n        val refreshAccessToken = RefreshAccessTokenUseCase(accessTokenRepository, userRepository, logOutUserUseCase)\n\n        // when\n        val result = useMockedAndroidLogger { refreshAccessToken() }\n\n        // then\n        verify(accessTokenRepository).refreshAccessToken()\n        verify(userRepository).promptUserReLogIn()\n        verify(logOutUserUseCase).invoke()\n        assertThat(result).isEqualTo(RefreshResult.Failure)\n    }\n\n    @Test\n    fun shouldRefreshAccessTokenWithError(): Unit = runBlocking {\n        // given\n        val accessTokenRepository = mock<AccessTokenRepository> {\n            onSuspend { refreshAccessToken() } doAnswer { throw UnknownHostException() }\n            onSuspend { clearAccessToken() } doReturn Unit\n        }\n\n        val userRepository = mock<UserRepository> {\n            onSuspend { promptUserReLogIn() } doReturn Unit\n        }\n\n        val logOutUserUseCase = mock<LogOutUserUseCase> {\n            onSuspend { invoke() } doReturn Unit\n        }\n\n        val refreshAccessToken = RefreshAccessTokenUseCase(accessTokenRepository, userRepository, logOutUserUseCase)\n\n        // when\n        val result = useMockedAndroidLogger { refreshAccessToken() }\n\n        // then\n        verify(accessTokenRepository).refreshAccessToken()\n        verify(logOutUserUseCase, times(0)).invoke()\n        assertThat(result).isInstanceOf(RefreshResult.Error::class.java)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/domain/library/SynchronizeLocalLibraryModificationsUseCaseTest.kt",
    "content": "package io.github.drumber.kitsune.domain.library\n\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.repository.LibraryRepository\nimport io.github.drumber.kitsune.testutils.libraryEntry\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport kotlinx.coroutines.test.runTest\nimport net.datafaker.Faker\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\n\nclass SynchronizeLocalLibraryModificationsUseCaseTest {\n\n    private val faker = Faker()\n\n    @Test\n    fun shouldSynchronizeAllLocalLibraryModifications() = runTest {\n        // given\n        val libraryEntryModifications = List(5) {\n            LibraryEntryModification\n                .withIdAndNulls(faker.internet().uuid())\n                .copy(status = LibraryStatus.entries.random())\n        }\n\n        val libraryRepository = mock<LibraryRepository> {\n            onSuspend { getAllLibraryEntryModifications() } doReturn libraryEntryModifications\n        }\n        val updateLibraryEntry = mock<UpdateLibraryEntryUseCase> {\n            onSuspend { invoke(any()) } doReturn LibraryEntryUpdateResult.Success(\n                libraryEntry(faker)\n            )\n        }\n\n        val synchronizeLocalLibraryModifications =\n            SynchronizeLocalLibraryModificationsUseCase(libraryRepository, updateLibraryEntry)\n\n        // when\n        val results = synchronizeLocalLibraryModifications()\n\n        // then\n        verify(libraryRepository).getAllLibraryEntryModifications()\n        verify(updateLibraryEntry, times(libraryEntryModifications.size)).invoke(any())\n\n        assertThat(results).containsOnlyKeys(libraryEntryModifications.map { it.id })\n        assertThat(results).allSatisfy { _, synchronizationResult ->\n            assertThat(synchronizationResult).isInstanceOf(LibraryEntryUpdateResult.Success::class.java)\n        }\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/domain/user/UpdateLocalUserUseCaseTest.kt",
    "content": "package io.github.drumber.kitsune.domain.user\n\nimport io.github.drumber.kitsune.data.repository.UserRepository\nimport io.github.drumber.kitsune.domain.auth.IsUserLoggedInUseCase\nimport io.github.drumber.kitsune.domain.auth.RefreshAccessTokenIfExpiredUseCase\nimport io.github.drumber.kitsune.testutils.onSuspend\nimport kotlinx.coroutines.runBlocking\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\n\nclass UpdateLocalUserUseCaseTest {\n\n    @Test\n    fun shouldUpdateLocalUser(): Unit = runBlocking {\n        // given\n        val userRepository = mock<UserRepository> {\n            onSuspend { fetchAndStoreLocalUserFromNetwork() } doReturn Unit\n        }\n\n        val isUserLoggedIn = mock<IsUserLoggedInUseCase> {\n            on { invoke() } doReturn true\n        }\n\n        val refreshAccessTokenIfExpired = mock<RefreshAccessTokenIfExpiredUseCase> {\n            onSuspend { invoke() } doReturn null\n        }\n\n        val updateLocalUser = UpdateLocalUserUseCase(userRepository, isUserLoggedIn, refreshAccessTokenIfExpired)\n\n        // when\n        updateLocalUser()\n\n        // then\n        verify(userRepository).fetchAndStoreLocalUserFromNetwork()\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/AndroidLoggerMock.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport android.util.Log\nimport org.mockito.Mockito\nimport org.mockito.kotlin.any\n\ninline fun <T, R> T.useMockedAndroidLogger(block: T.() -> R): R = Mockito.mockStatic(Log::class.java).use {\n    it.`when`<Int> { Log.v(any(), any()) }.thenReturn(0)\n    it.`when`<Int> { Log.v(any(), any(), any()) }.thenReturn(0)\n\n    it.`when`<Int> { Log.d(any(), any()) }.thenReturn(0)\n    it.`when`<Int> { Log.d(any(), any(), any()) }.thenReturn(0)\n\n    it.`when`<Int> { Log.i(any(), any()) }.thenReturn(0)\n    it.`when`<Int> { Log.i(any(), any(), any()) }.thenReturn(0)\n\n    it.`when`<Int> { Log.w(any(), any<String>()) }.thenReturn(0)\n    it.`when`<Int> { Log.w(any(), any(), any()) }.thenReturn(0)\n\n    it.`when`<Int> { Log.e(any(), any()) }.thenReturn(0)\n    it.`when`<Int> { Log.e(any(), any(), any()) }.thenReturn(0)\n\n    block()\n}\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/FakeCharacter.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport io.github.drumber.kitsune.data.presentation.model.character.Character\nimport io.github.drumber.kitsune.data.presentation.model.character.MediaCharacter\nimport io.github.drumber.kitsune.data.presentation.model.character.MediaCharacterRole\nimport io.github.drumber.kitsune.data.source.local.character.LocalCharacter\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkCharacter\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkMediaCharacter\nimport io.github.drumber.kitsune.data.source.network.character.model.NetworkMediaCharacterRole\nimport net.datafaker.Faker\n\nfun networkCharacter(faker: Faker) = NetworkCharacter(\n    id = faker.number().positive().toString(),\n    slug = faker.internet().slug(),\n    name = faker.name().name(),\n    names = mapOf(\"en\" to faker.name().name()),\n    otherNames = listOf(faker.name().name(), faker.name().name()),\n    malId = faker.number().positive(),\n    description = faker.lorem().sentence(),\n    image = image(faker),\n    mediaCharacters = listOf(\n        NetworkMediaCharacter(\n            id = faker.number().positive().toString(),\n            role = NetworkMediaCharacterRole.entries.random(),\n            media = null\n        )\n    )\n)\n\nfun localCharacter(faker: Faker) = LocalCharacter(\n    id = faker.number().positive().toString(),\n    slug = faker.internet().slug(),\n    name = faker.name().name(),\n    names = mapOf(\"en\" to faker.name().name()),\n    otherNames = listOf(faker.name().name(), faker.name().name()),\n    malId = faker.number().positive(),\n    description = faker.lorem().sentence(),\n    image = image(faker)\n)\n\nfun character(faker: Faker) = Character(\n    id = faker.number().positive().toString(),\n    slug = faker.internet().slug(),\n    name = faker.name().name(),\n    names = mapOf(\"en\" to faker.name().name()),\n    otherNames = listOf(faker.name().name(), faker.name().name()),\n    malId = faker.number().positive(),\n    description = faker.lorem().sentence(),\n    image = image(faker),\n    mediaCharacters = listOf(\n        MediaCharacter(\n            id = faker.number().positive().toString(),\n            role = MediaCharacterRole.entries.random(),\n            media = null\n        )\n    )\n)"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/FakeImage.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport io.github.drumber.kitsune.data.common.ImageDimension\nimport io.github.drumber.kitsune.data.common.ImageDimensions\nimport net.datafaker.Faker\n\nfun image(faker: Faker) = io.github.drumber.kitsune.data.common.Image(\n    tiny = faker.internet().image(),\n    small = faker.internet().image(),\n    medium = faker.internet().image(),\n    large = faker.internet().image(),\n    original = faker.internet().image(),\n    meta = io.github.drumber.kitsune.data.common.ImageMeta(\n        ImageDimensions(\n            tiny = ImageDimension(faker.number().positive(), faker.number().positive()),\n            small = ImageDimension(faker.number().positive(), faker.number().positive()),\n            medium = ImageDimension(faker.number().positive(), faker.number().positive()),\n            large = ImageDimension(faker.number().positive(), faker.number().positive())\n        )\n    )\n)\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/FakeLibraryEntry.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport io.github.drumber.kitsune.data.presentation.model.media.Media\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntry\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryMedia\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryStatus\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalReactionSkip\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryEntry\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkLibraryStatus\nimport io.github.drumber.kitsune.data.source.network.library.model.NetworkReactionSkip\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkMedia\nimport io.github.drumber.kitsune.util.DATE_FORMAT_ISO\nimport net.datafaker.Faker\n\nfun libraryEntry(faker: Faker, media: Media? = null) = io.github.drumber.kitsune.data.presentation.model.library.LibraryEntry(\n    id = faker.number().positive().toString(),\n    updatedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    startedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    finishedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    progressedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    status = io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus.entries.random(),\n    progress = faker.number().positive(),\n    reconsuming = faker.bool().bool(),\n    reconsumeCount = faker.number().positive(),\n    volumesOwned = faker.number().positive(),\n    ratingTwenty = faker.number().numberBetween(0, 20),\n    notes = faker.text().text(),\n    privateEntry = faker.bool().bool(),\n    reactionSkipped = io.github.drumber.kitsune.data.presentation.model.library.ReactionSkip.entries.random(),\n    media = media\n)\n\nfun networkLibraryEntry(faker: Faker, media: NetworkMedia? = null) = NetworkLibraryEntry(\n    id = faker.number().positive().toString(),\n    updatedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    startedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    finishedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    progressedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    status = NetworkLibraryStatus.entries.random(),\n    progress = faker.number().positive(),\n    reconsuming = faker.bool().bool(),\n    reconsumeCount = faker.number().positive(),\n    volumesOwned = faker.number().positive(),\n    ratingTwenty = faker.number().numberBetween(0, 20),\n    notes = faker.text().text(),\n    privateEntry = faker.bool().bool(),\n    reactionSkipped = NetworkReactionSkip.entries.random(),\n    anime = if (media is NetworkAnime) media else null,\n    manga = if (media is NetworkManga) media else null,\n    user = null\n)\n\nfun localLibraryEntry(faker: Faker, media: LocalLibraryMedia? = null) = LocalLibraryEntry(\n    id = faker.number().positive().toString(),\n    updatedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    startedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    finishedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    progressedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    status = LocalLibraryStatus.entries.random(),\n    progress = faker.number().positive(),\n    reconsuming = faker.bool().bool(),\n    reconsumeCount = faker.number().positive(),\n    volumesOwned = faker.number().positive(),\n    ratingTwenty = faker.number().numberBetween(0, 20),\n    notes = faker.text().text(),\n    privateEntry = faker.bool().bool(),\n    reactionSkipped = LocalReactionSkip.entries.random(),\n    media = media\n)\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/FakeLibraryEntryModification.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryEntryModification\nimport io.github.drumber.kitsune.data.presentation.model.library.LibraryStatus\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryEntryModification\nimport io.github.drumber.kitsune.data.source.local.library.model.LocalLibraryStatus\nimport io.github.drumber.kitsune.util.DATE_FORMAT_ISO\nimport net.datafaker.Faker\n\nfun libraryEntryModification(faker: Faker) = LibraryEntryModification(\n    id = faker.number().positive().toString(),\n    startedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    finishedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    status = LibraryStatus.entries.random(),\n    progress = faker.number().positive(),\n    reconsumeCount = faker.number().positive(),\n    volumesOwned = faker.number().positive(),\n    ratingTwenty = faker.number().numberBetween(1, 20),\n    notes = faker.text().text(),\n    privateEntry = faker.bool().bool()\n)\n\nfun localLibraryEntryModification(faker: Faker) = LocalLibraryEntryModification(\n    id = faker.number().positive().toString(),\n    startedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    finishedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    status = LocalLibraryStatus.entries.random(),\n    progress = faker.number().positive(),\n    reconsumeCount = faker.number().positive(),\n    volumesOwned = faker.number().positive(),\n    ratingTwenty = faker.number().numberBetween(1, 20),\n    notes = faker.text().text(),\n    privateEntry = faker.bool().bool()\n)\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/FakeMedia.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport io.github.drumber.kitsune.data.common.Titles\nimport io.github.drumber.kitsune.data.common.media.AgeRating\nimport io.github.drumber.kitsune.data.common.media.AnimeSubtype\nimport io.github.drumber.kitsune.data.common.media.MangaSubtype\nimport io.github.drumber.kitsune.data.common.media.RatingFrequencies\nimport io.github.drumber.kitsune.data.common.media.ReleaseStatus\nimport io.github.drumber.kitsune.data.presentation.model.media.Anime\nimport io.github.drumber.kitsune.data.presentation.model.media.Manga\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnime\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkAnimeSubtype\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkManga\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkMangaSubtype\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkRatingFrequencies\nimport io.github.drumber.kitsune.data.source.network.media.model.NetworkReleaseStatus\nimport io.github.drumber.kitsune.util.DATE_FORMAT_ISO\nimport net.datafaker.Faker\nimport java.util.Locale\nimport java.util.concurrent.TimeUnit\n\nfun networkAnime(faker: Faker) = NetworkAnime(\n    id = faker.number().positive().toString(),\n    slug = faker.internet().slug(),\n    description = faker.text().text(),\n    titles = titles(),\n    canonicalTitle = faker.book().title(),\n    abbreviatedTitles = faker.collection({ faker.book().title() }).maxLen(4).build().toList(),\n    averageRating = faker.number().positive().toString(),\n    ratingFrequencies = networkRatingFrequencies(faker),\n    userCount = faker.number().positive(),\n    favoritesCount = faker.number().positive(),\n    popularityRank = faker.number().positive(),\n    ratingRank = faker.number().positive(),\n    startDate = faker.date().birthday(DATE_FORMAT_ISO),\n    endDate = faker.date().birthday(DATE_FORMAT_ISO),\n    nextRelease = faker.date().birthday(DATE_FORMAT_ISO),\n    tba = faker.date().future(360, TimeUnit.DAYS, \"MMMMM yyyy\"),\n    status = NetworkReleaseStatus.entries.random(),\n    ageRating = AgeRating.entries.random(),\n    ageRatingGuide = faker.text().text(5),\n    nsfw = faker.bool().bool(),\n    posterImage = image(faker),\n    coverImage = image(faker),\n    totalLength = faker.number().positive(),\n    episodeCount = faker.number().positive(),\n    episodeLength = faker.number().positive(),\n    youtubeVideoId = faker.text().text(5, 10, true),\n    subtype = NetworkAnimeSubtype.entries.random(),\n    categories = null,\n    animeProduction = null,\n    streamingLinks = null,\n    mediaRelationships = null\n)\n\nfun anime(faker: Faker) = Anime(\n    id = faker.number().positive().toString(),\n    slug = faker.internet().slug(),\n    description = faker.text().text(),\n    titles = titles(),\n    canonicalTitle = faker.book().title(),\n    abbreviatedTitles = faker.collection({ faker.book().title() }).maxLen(4).build().toList(),\n    averageRating = faker.number().positive().toString(),\n    ratingFrequencies = newRatingFrequencies(faker),\n    userCount = faker.number().positive(),\n    favoritesCount = faker.number().positive(),\n    popularityRank = faker.number().positive(),\n    ratingRank = faker.number().positive(),\n    startDate = faker.date().birthday(DATE_FORMAT_ISO),\n    endDate = faker.date().birthday(DATE_FORMAT_ISO),\n    nextRelease = faker.date().birthday(DATE_FORMAT_ISO),\n    tba = faker.date().future(360, TimeUnit.DAYS, \"MMMMM yyyy\"),\n    status = ReleaseStatus.entries.random(),\n    ageRating = AgeRating.entries.random(),\n    ageRatingGuide = faker.text().text(5),\n    nsfw = faker.bool().bool(),\n    posterImage = image(faker),\n    coverImage = image(faker),\n    totalLength = faker.number().positive(),\n    episodeCount = faker.number().positive(),\n    episodeLength = faker.number().positive(),\n    youtubeVideoId = faker.text().text(5, 10, true),\n    subtype = AnimeSubtype.entries.random(),\n    categories = null,\n    animeProduction = null,\n    streamingLinks = null,\n    mediaRelationships = null\n)\n\nfun networkManga(faker: Faker) = NetworkManga(\n    id = faker.number().positive().toString(),\n    slug = faker.internet().slug(),\n    description = faker.text().text(),\n    titles = titles(),\n    canonicalTitle = faker.book().title(),\n    abbreviatedTitles = faker.collection({ faker.book().title() }).maxLen(4).build().toList(),\n    averageRating = faker.number().positive().toString(),\n    ratingFrequencies = networkRatingFrequencies(faker),\n    userCount = faker.number().positive(),\n    favoritesCount = faker.number().positive(),\n    popularityRank = faker.number().positive(),\n    ratingRank = faker.number().positive(),\n    startDate = faker.date().birthday(DATE_FORMAT_ISO),\n    endDate = faker.date().birthday(DATE_FORMAT_ISO),\n    nextRelease = faker.date().birthday(DATE_FORMAT_ISO),\n    tba = faker.date().future(360, TimeUnit.DAYS, \"MMMMM yyyy\"),\n    status = NetworkReleaseStatus.entries.random(),\n    ageRating = AgeRating.R,\n    ageRatingGuide = faker.text().text(5),\n    nsfw = faker.bool().bool(),\n    posterImage = image(faker),\n    coverImage = image(faker),\n    totalLength = faker.number().positive(),\n    subtype = NetworkMangaSubtype.entries.random(),\n    chapterCount = faker.number().positive(),\n    volumeCount = faker.number().positive(),\n    serialization = faker.book().publisher(),\n    categories = null,\n    mediaRelationships = null\n)\n\nfun manga(faker: Faker) = Manga(\n    id = faker.number().positive().toString(),\n    slug = faker.internet().slug(),\n    description = faker.text().text(),\n    titles = titles(),\n    canonicalTitle = faker.book().title(),\n    abbreviatedTitles = faker.collection({ faker.book().title() }).maxLen(4).build().toList(),\n    averageRating = faker.number().positive().toString(),\n    ratingFrequencies = newRatingFrequencies(faker),\n    userCount = faker.number().positive(),\n    favoritesCount = faker.number().positive(),\n    popularityRank = faker.number().positive(),\n    ratingRank = faker.number().positive(),\n    startDate = faker.date().birthday(DATE_FORMAT_ISO),\n    endDate = faker.date().birthday(DATE_FORMAT_ISO),\n    nextRelease = faker.date().birthday(DATE_FORMAT_ISO),\n    tba = faker.date().future(360, TimeUnit.DAYS, \"MMMMM yyyy\"),\n    status = ReleaseStatus.entries.random(),\n    ageRating = AgeRating.R,\n    ageRatingGuide = faker.text().text(5),\n    nsfw = faker.bool().bool(),\n    posterImage = image(faker),\n    coverImage = image(faker),\n    totalLength = faker.number().positive(),\n    subtype = MangaSubtype.entries.random(),\n    chapterCount = faker.number().positive(),\n    volumeCount = faker.number().positive(),\n    serialization = faker.book().publisher(),\n    categories = null,\n    mediaRelationships = null\n)\n\nfun networkRatingFrequencies(faker: Faker) = NetworkRatingFrequencies(\n    r2 = faker.number().positive().toString(),\n    r3 = faker.number().positive().toString(),\n    r4 = faker.number().positive().toString(),\n    r5 = faker.number().positive().toString(),\n    r6 = faker.number().positive().toString(),\n    r7 = faker.number().positive().toString(),\n    r8 = faker.number().positive().toString(),\n    r9 = faker.number().positive().toString(),\n    r10 = faker.number().positive().toString(),\n    r11 = faker.number().positive().toString(),\n    r12 = faker.number().positive().toString(),\n    r13 = faker.number().positive().toString(),\n    r14 = faker.number().positive().toString(),\n    r15 = faker.number().positive().toString(),\n    r16 = faker.number().positive().toString(),\n    r17 = faker.number().positive().toString(),\n    r18 = faker.number().positive().toString(),\n    r19 = faker.number().positive().toString(),\n    r20 = faker.number().positive().toString()\n)\n\nfun newRatingFrequencies(faker: Faker) = RatingFrequencies(\n    r2 = faker.number().positive().toString(),\n    r3 = faker.number().positive().toString(),\n    r4 = faker.number().positive().toString(),\n    r5 = faker.number().positive().toString(),\n    r6 = faker.number().positive().toString(),\n    r7 = faker.number().positive().toString(),\n    r8 = faker.number().positive().toString(),\n    r9 = faker.number().positive().toString(),\n    r10 = faker.number().positive().toString(),\n    r11 = faker.number().positive().toString(),\n    r12 = faker.number().positive().toString(),\n    r13 = faker.number().positive().toString(),\n    r14 = faker.number().positive().toString(),\n    r15 = faker.number().positive().toString(),\n    r16 = faker.number().positive().toString(),\n    r17 = faker.number().positive().toString(),\n    r18 = faker.number().positive().toString(),\n    r19 = faker.number().positive().toString(),\n    r20 = faker.number().positive().toString()\n)\n\nfun titles(): Titles = buildMap {\n    put(\"en\", Faker(Locale.ENGLISH).book().title())\n    put(\"en_jp\", Faker(Locale.ENGLISH).book().title())\n    put(\"ja_jp\", Faker(Locale.JAPANESE).book().title())\n    val faker = Faker()\n    repeat(faker.random().nextInt(0, 5)) {\n        put(faker.locality().localeString(), faker.book().title())\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/FakeUser.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport io.github.drumber.kitsune.data.common.user.UserThemePreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalSfwFilterPreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalTitleLanguagePreference\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalUser\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavorite\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkFavoriteItem\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkRatingSystemPreference\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkSfwFilterPreference\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkTitleLanguagePreference\nimport io.github.drumber.kitsune.data.source.network.user.model.NetworkUser\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLink\nimport io.github.drumber.kitsune.data.source.network.user.model.profilelinks.NetworkProfileLinkSite\nimport io.github.drumber.kitsune.util.DATE_FORMAT_ISO\nimport net.datafaker.Faker\n\nfun networkUser(faker: Faker) = NetworkUser(\n    id = faker.number().positive().toString(),\n    createdAt = faker.date().birthday(DATE_FORMAT_ISO),\n    updatedAt = faker.date().birthday(DATE_FORMAT_ISO),\n    name = faker.name().name(),\n    slug = faker.internet().slug(),\n    email = faker.internet().emailAddress(),\n    title = faker.name().title(),\n    avatar = image(faker),\n    coverImage = image(faker),\n    about = faker.text().text(),\n    location = faker.country().name(),\n    gender = faker.gender().types(),\n    birthday = faker.date().birthday(DATE_FORMAT_ISO),\n    waifuOrHusbando = null,\n    followersCount = faker.number().positive(),\n    followingCount = faker.number().positive(),\n    commentsCount = faker.number().positive(),\n    favoritesCount = faker.number().positive(),\n    likesGivenCount = faker.number().positive(),\n    reviewsCount = faker.number().positive(),\n    likesReceivedCount = faker.number().positive(),\n    postsCount = faker.number().positive(),\n    ratingsCount = faker.number().positive(),\n    mediaReactionsCount = faker.number().positive(),\n    country = faker.country().countryCode2(),\n    language = faker.locality().localeString(),\n    timeZone = null,\n    theme = UserThemePreference.Light,\n    sfwFilter = false,\n    ratingSystem = NetworkRatingSystemPreference.Regular,\n    shareToGlobal = null,\n    sfwFilterPreference = NetworkSfwFilterPreference.SFW,\n    titleLanguagePreference = NetworkTitleLanguagePreference.English,\n    profileCompleted = true,\n    feedCompleted = true,\n    proTier = null,\n    proExpiresAt = null,\n    aoPro = null,\n    facebookId = null,\n    confirmed = null,\n    status = null,\n    hasPassword = true,\n    subscribedToNewsletter = false,\n    stats = null,\n    favorites = listOf(networkFavorite(faker, null), networkFavorite(faker, null)),\n    waifu = networkCharacter(faker),\n    profileLinks = listOf(networkProfileLink(faker, null))\n)\n\nfun localUser(faker: Faker) = LocalUser(\n    id = faker.number().positive().toString(),\n    createdAt = faker.date().birthday(DATE_FORMAT_ISO),\n    name = faker.name().name(),\n    slug = faker.internet().slug(),\n    email = faker.internet().emailAddress(),\n    title = faker.name().title(),\n    avatar = image(faker),\n    coverImage = image(faker),\n    about = faker.text().text(),\n    location = faker.country().name(),\n    gender = faker.gender().types(),\n    birthday = faker.date().birthday(DATE_FORMAT_ISO),\n    waifuOrHusbando = null,\n    followersCount = faker.number().positive(),\n    followingCount = faker.number().positive(),\n    country = faker.country().countryCode2(),\n    language = faker.locality().localeString(),\n    timeZone = null,\n    theme = UserThemePreference.Light,\n    sfwFilter = false,\n    ratingSystem = LocalRatingSystemPreference.Regular,\n    sfwFilterPreference = LocalSfwFilterPreference.SFW,\n    titleLanguagePreference = LocalTitleLanguagePreference.English,\n    waifu = localCharacter(faker),\n)\n\nfun networkFavorite(\n    faker: Faker,\n    user: NetworkUser? = networkUser(faker),\n    item: NetworkFavoriteItem? = networkAnime(faker)\n) = NetworkFavorite(\n    id = faker.number().positive().toString(),\n    favRank = faker.number().positive(),\n    item = item,\n    user = user\n)\n\nfun networkProfileLink(faker: Faker, user: NetworkUser? = networkUser(faker)) = NetworkProfileLink(\n    id = faker.number().positive().toString(),\n    url = faker.internet().url(),\n    profileLinkSite = NetworkProfileLinkSite(\n        id = faker.number().positive().toString(),\n        name = faker.company().name()\n    ),\n    user = user\n)\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/KoinTestModules.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport io.github.drumber.kitsune.testutils.network.NoOpAuthenticationInterceptor\nimport io.github.drumber.kitsune.util.network.AuthenticationInterceptor\nimport org.koin.dsl.module\n\nval noOpAuthenticatorInterceptor = module {\n    factory<AuthenticationInterceptor> { NoOpAuthenticationInterceptor() }\n}\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/SuspendFunctionHelper.kt",
    "content": "package io.github.drumber.kitsune.testutils\n\nimport kotlinx.coroutines.runBlocking\nimport org.assertj.core.api.AbstractThrowableAssert\nimport org.assertj.core.api.ThrowableAssert\nimport org.mockito.kotlin.KStubbing\nimport kotlin.test.assertFails\n\nfun <T : Any, R> KStubbing<T>.onSuspend(methodCall: suspend T.() -> R) =\n    on { runBlocking { methodCall() } }\n\nsuspend fun assertThatThrownBy(shouldRaiseThrowable: suspend () -> Unit): AbstractThrowableAssert<*, out Throwable> {\n    val throwable = assertFails { shouldRaiseThrowable() }\n    return CustomThrowableAssert(throwable).hasBeenThrown()\n}\n\nclass CustomThrowableAssert<T : Throwable>(actual: T?) : ThrowableAssert<T>(actual) {\n    public override fun hasBeenThrown(): ThrowableAssert<T> {\n        return super.hasBeenThrown()\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/network/FakeHttpException.kt",
    "content": "package io.github.drumber.kitsune.testutils.network\n\nimport okhttp3.ResponseBody.Companion.toResponseBody\nimport retrofit2.HttpException\nimport retrofit2.Response\n\nclass FakeHttpException(code: Int) : HttpException(\n    Response.error<Any>(code, \"\".toResponseBody())\n)"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/testutils/network/NoOpAuthenticationInterceptor.kt",
    "content": "package io.github.drumber.kitsune.testutils.network\n\nimport io.github.drumber.kitsune.util.network.AuthenticationInterceptor\nimport okhttp3.Authenticator\nimport okhttp3.Interceptor\nimport okhttp3.Response\n\nclass NoOpAuthenticationInterceptor : AuthenticationInterceptor,\n    Authenticator by Authenticator.NONE {\n    override fun intercept(chain: Interceptor.Chain): Response {\n        return chain.proceed(chain.request())\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/util/DateUtilTest.kt",
    "content": "package io.github.drumber.kitsune.util\n\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\nimport java.time.ZoneOffset\nimport java.util.Calendar\nimport java.util.TimeZone\n\nclass DateUtilTest {\n\n    @Test\n    fun shouldFormatISODate() {\n        // given\n        val expectedDateString = \"2023-11-12T12:42:12.123Z\"\n        val time = Calendar.getInstance().apply {\n            set(2023, Calendar.NOVEMBER, 12, 13, 42, 12)\n            set(Calendar.MILLISECOND, 123)\n            timeZone = TimeZone.getTimeZone(ZoneOffset.of(\"+1\"))\n        }\n\n        // when\n        val dateString = time.formatUtcDate()\n\n        // then\n        assertThat(dateString).isEqualTo(expectedDateString)\n    }\n\n    @Test\n    fun shouldParseISODate() {\n        // given\n        val time = Calendar.getInstance().apply {\n            set(2023, Calendar.NOVEMBER, 12, 13, 42, 12)\n            set(Calendar.MILLISECOND, 123)\n            timeZone = TimeZone.getTimeZone(ZoneOffset.of(\"+1\"))\n        }.time\n        val isoDateString = time.formatUtcDate()\n\n        // when\n        val parsedDate = isoDateString.parseUtcDate()\n\n        // then\n        assertThat(parsedDate).isEqualTo(time)\n    }\n\n}\n"
  },
  {
    "path": "app/src/test/java/io/github/drumber/kitsune/util/rating/RatingFrequenciesUtilTest.kt",
    "content": "package io.github.drumber.kitsune.util.rating\n\nimport io.github.drumber.kitsune.data.common.media.RatingFrequencies\nimport io.github.drumber.kitsune.data.source.local.user.model.LocalRatingSystemPreference\nimport io.github.drumber.kitsune.util.rating.RatingFrequenciesUtil.calculateAverageRating\nimport io.github.drumber.kitsune.util.rating.RatingFrequenciesUtil.transformToRatingSystem\nimport org.assertj.core.api.Assertions.assertThat\nimport org.junit.Test\n\nclass RatingFrequenciesUtilTest {\n\n    @Test\n    fun shouldTransformToRatingSystem() {\n        // given\n        val ratings = ratingFrequencies(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)\n        val parameters = mapOf(\n            LocalRatingSystemPreference.Simple to listOf(27, 63, 99, 20),\n            LocalRatingSystemPreference.Regular to listOf(5, 9, 13, 17, 21, 25, 29, 33, 37, 20),\n            LocalRatingSystemPreference.Advanced to listOf(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)\n        )\n\n        parameters.forEach { (ratingSystem, expectedOutput) ->\n            // when\n            val mappedRatings = ratings.transformToRatingSystem(ratingSystem)\n\n            // then\n            assertThat(mappedRatings).`as`(\"RatingSystem '${ratingSystem.name}'\").isEqualTo(expectedOutput)\n        }\n    }\n\n    @Test\n    fun shouldCalculateAverageRating() {\n        // given\n        val testScenarios = listOf(\n            TestScenario(\n                input = ratingFrequencies(10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10),\n                expectedOutput = mapOf(\n                    LocalRatingSystemPreference.Simple to 2.5,\n                    LocalRatingSystemPreference.Regular to 2.75,\n                    LocalRatingSystemPreference.Advanced to 5.5\n                )\n            ),\n            TestScenario(\n                input = ratingFrequencies(1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 3, 1, 4, 1, 0, 0, 0, 1, 1),\n                expectedOutput = mapOf(\n                    LocalRatingSystemPreference.Simple to 2.3125,\n                    LocalRatingSystemPreference.Regular to 2.9375,\n                    LocalRatingSystemPreference.Advanced to 6.0\n                )\n            )\n        )\n\n        testScenarios.forEach { (ratings, testCases) ->\n            testCases.forEach { (ratingSystem, expectedOutput) ->\n                // when\n                val average = ratings.calculateAverageRating(ratingSystem)\n\n                // then\n                assertThat(average).`as`(\"RatingSystem '${ratingSystem.name}'\").isEqualTo(expectedOutput)\n            }\n        }\n    }\n\n    private fun ratingFrequencies(vararg ratingCounts: Int): RatingFrequencies {\n        require(ratingCounts.size == 19)\n        val r = ratingCounts.map(Int::toString)\n        return RatingFrequencies(\n            r[0],\n            r[1],\n            r[2],\n            r[3],\n            r[4],\n            r[5],\n            r[6],\n            r[7],\n            r[8],\n            r[9],\n            r[10],\n            r[11],\n            r[12],\n            r[13],\n            r[14],\n            r[15],\n            r[16],\n            r[17],\n            r[18]\n        )\n    }\n\n    data class TestScenario<out INPUT, out OUTPUT>(\n        val input: INPUT,\n        val expectedOutput: OUTPUT\n    )\n}"
  },
  {
    "path": "app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker",
    "content": "mock-maker-inline"
  },
  {
    "path": "build.gradle.kts",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nplugins {\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.android.legacy.kapt) apply false\n    alias(libs.plugins.ksp) apply false\n    alias(libs.plugins.androidx.navigation.safeargs) apply false\n    alias(libs.plugins.jetbrains.kotlin.parcelize) apply false\n    alias(libs.plugins.jetbrains.kotlin.serialization) apply false\n    alias(libs.plugins.aboutlibraries.plugin) apply false\n}\n"
  },
  {
    "path": "docs/hidden-actions.md",
    "content": "# Hidden Actions\n\n### All pages\nClicking on the selected menu item in the bottom navigation...\n- **scrolls up** if not already at the top\n- **navigates back** when on a subpage (or triggers a different action depending on the page)\n\n### Search page\n- Clicking on the selected menu item in the bottom navigation **focuses the search field**\n- Long-clicking on the filter icon in the search bar **resets all filters**\n\n### Library page\n- Long-clicking on a library entry **opens the details**\n\n### Profile and Anime/Manga page\n- Clicking on an image opens it in a preview\n"
  },
  {
    "path": "fastlane/Appfile",
    "content": "json_key_file(\"\") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one\npackage_name(\"io.github.drumber.kitsune\")\n"
  },
  {
    "path": "fastlane/Fastfile",
    "content": "opt_out_usage\n# This file contains the fastlane.tools configuration\n# You can find the documentation at https://docs.fastlane.tools\n#\n# For a list of all available actions, check out\n#\n#     https://docs.fastlane.tools/actions\n#\n# For a list of all available plugins, check out\n#\n#     https://docs.fastlane.tools/plugins/available-plugins\n#\n\n# Uncomment the line if you want fastlane to automatically update itself\n# update_fastlane\n\ndefault_platform(:android)\n\nplatform :android do\n  desc \"Runs all the tests\"\n  lane :test do\n    gradle(task: \"test\")\n  end\n\n  desc \"Build debug and test APK for screenshots\"\n  lane :build_and_screengrab do\n    build_android_app(\n      task: 'assemble',\n      build_type: 'Instrumented',\n      properties: {\n        'screenshotMode' => 'true'\n      }\n    )\n    build_android_app(\n      task: 'assemble',\n      build_type: 'AndroidTest',\n      properties: {\n        'screenshotMode' => 'true'\n      }\n    )\n    do_screengrab()\n  end\n  \n  desc \"Take screenshots\"\n  lane :do_screengrab do\n    screengrab(\n      output_directory: 'fastlane/screenshots',\n      use_timestamp_suffix: false,\n      app_package_name: 'io.github.drumber.kitsune.instrumented',\n      tests_package_name: 'io.github.drumber.kitsune.instrumented.test',\n      use_tests_in_packages: 'io.github.drumber.kitsune.fastlane',\n      app_apk_path: 'app/build/outputs/apk/instrumented/app-instrumented.apk',\n      tests_apk_path: 'app/build/outputs/apk/androidTest/instrumented/app-instrumented-androidTest.apk',\n      reinstall_app: true,\n      clear_previous_screenshots: true\n    )\n  end\n\n  desc \"Add a device frame to the screenshots and copy them to the phoneScreenshots and media folders\"\n  lane :process_screenshots do\n    frameit(\n      path: 'fastlane/screenshots'\n    )\n  \tcopy_artifacts(\n      target_path: \"media\",\n      artifacts: [\"fastlane/screenshots/en-US/images/phoneScreenshots/*_framed.png\"]\n\t  )\n\t  copy_artifacts(\n      target_path: \"fastlane/metadata/android/en-US/images/phoneScreenshots\",\n      artifacts: [\"fastlane/screenshots/en-US/images/phoneScreenshots/*_framed.png\"]\n    )\n  end\nend\n"
  },
  {
    "path": "fastlane/README.md",
    "content": "fastlane documentation\n----\n\n# Installation\n\nMake sure you have the latest version of the Xcode command line tools installed:\n\n```sh\nxcode-select --install\n```\n\nFor _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)\n\n# Available Actions\n\n## Android\n\n### android test\n\n```sh\n[bundle exec] fastlane android test\n```\n\nRuns all the tests\n\n### android build_and_screengrab\n\n```sh\n[bundle exec] fastlane android build_and_screengrab\n```\n\nBuild debug and test APK for screenshots\n\n### android do_screengrab\n\n```sh\n[bundle exec] fastlane android do_screengrab\n```\n\nTake screenshots\n\n### android process_screenshots\n\n```sh\n[bundle exec] fastlane android process_screenshots\n```\n\nAdd a device frame to the screenshots and copy them to the phoneScreenshots and media folders\n\n----\n\nThis README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.\n\nMore information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).\n\nThe documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).\n"
  },
  {
    "path": "fastlane/Workflow.md",
    "content": "# Fastlane Workflow\n\n> This assumes that you have installed [Fastlane](https://docs.fastlane.tools/) using Bundler.\n\nIf you're getting 'Permission denied' errors from adb, restart the adb daemon as root: `adb root`\n\n## Capture Screenshots\n1. Make sure the emulator is running (preferably Pixel 5 with API 32+)\n2. Make sure System UI demo mode is enabled in developer settings\n3. Run `bundle exec fastlane android build_and_screengrab` to build the debug/test apks and run the instrumentation test on the device\n\n## Process Screenshots\n1. This actions assumes that the screenshots have been created in the `fastlane/screenshots` directory\n2. Run `bundle exec fastlane android process_screenshots` to add a device frame to each screenshot and copy the framed screenshots to the `media` folder\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/25.txt",
    "content": "* Added character details bottom sheet\n* Added external site links to anime and manga details\n* Added heart animation when favoriting a character or anime/manga\n* Minor improvements and other changes\n* Exclude access token preference from auto backups\n* Updated dependencies"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/26.txt",
    "content": "New changes:\n* Fixed visible list refreshing when navigating back to the library\n* Updated dependencies\n\nChanges from v1.10.3:\n* Added character details bottom sheet\n* Added external site links to anime and manga details\n* Added heart animation when favoriting a character or anime/manga\n* Minor improvements and other changes\n* Exclude access token preference from auto backups\n* Updated dependencies"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/27.txt",
    "content": "New changes:\n* Disabled update checker by default\n* Fixed visible list refreshing when navigating back to the library\n* Updated dependencies\n\nChanges from v1.10.3:\n* Added character details bottom sheet\n* Added external site links to anime and manga details\n* Added heart animation when favoriting a character or anime/manga\n* Minor improvements and other changes\n* Exclude access token preference from auto backups\n* Updated dependencies"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/28.txt",
    "content": "New changes:\n* Disabled update checker by default\n* Fixed visible list refreshing when navigating back to the library\n* Updated dependencies\n\nChanges from v1.10.3:\n* Added character details bottom sheet\n* Added external site links to anime and manga details\n* Added heart animation when favoriting a character or anime/manga\n* Minor improvements and other changes\n* Exclude access token preference from auto backups\n* Updated dependencies"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/29.txt",
    "content": "* Added Chinese translation (Thanks @yzqzss)\n* Added Turkish translation (Thanks @m3raa)\n* Added Vietnamese translation (Thanks @ngocanhtve)\n* Added German translation\n* Added support for per-app language (Android 13+)\n* Added language picker to settings\n* Show correct status labels when the library is filtered by manga only\n* Show average rating above the rating chart\n* Added menu to change the rating system for the rating chart\n* Updated layout of the \"edit library entry\" dialog\n* Enabled HTTP caching\n* Fixed refreshing of the access token"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/30.txt",
    "content": "* Added French translation (Thanks @IsAy15)\n* Fixed a crash when opening the \"Characters\" page"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/31.txt",
    "content": "* Fixed a bug with the login\n\nChanges from v1.10.6:\n* Added French translation (Thanks @IsAy15)\n* Fixed a crash when opening the \"Characters\" page"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/32.txt",
    "content": "* Major code refactoring to increase the maintainability and expandability of the app\n* Added home screen widget\n* Display library with multiple columns on large screens\n* Enabled APK minification to reduce the app size\n* Bug fixes\n* Updated French translations by @IsAy15\n* Added Spanish translations by @Kiimby (21% translated)\n* Added Portuguese translations by @AsmodeumX (4% translated)"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/33.txt",
    "content": "Note: The API domain has changed from kitsu.io to kitsu.app. Official statement: https://kitsu.app/posts/9837041\n\n* Updated Kitsu domain (moved from kitsu.io to kitsu.app)\n* Fixed app shortcuts\n* Updated Spanish translation by @Kiimby\n* Updated Turkish translation by @oersen\n* Updated Portuguese translation by Jonathan"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/34.txt",
    "content": "* Added support for Android 15\n* Added onboarding on first launch\n* Added more transition animations between screens\n* Added Russian translation by @Xapitonov\n* Updated Spanish translation by gallegonovato\n* Updated Portuguese translation by Crono\n* Updated French translation by @GitSpoon\n* Updated German translation\n* Fixed widget not updated on login and logout\n* Bug fixes and small improvements"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/35.txt",
    "content": "* Added support for Android 15\n* Added onboarding on first launch\n* Added more transition animations between screens\n* Added Russian translation by @Xapitonov\n* Updated Spanish translation by gallegonovato\n* Updated Portuguese translation by Crono\n* Updated French translation by @GitSpoon\n* Updated German translation\n* Fixed widget not updated on login and logout\n* Bug fixes and small improvements\n* Disabled dependency info block in APK signature"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/36.txt",
    "content": "* Fixed a potential crash when navigating back to the search screen\n\nChanges from v2.0.2:\n* Added support for Android 15\n* Added onboarding on first launch\n* Added more transition animations between screens\n* Added Russian translation by @Xapitonov\n* Updated Spanish translation by gallegonovato\n* Updated Portuguese translation by Crono\n* Updated French translation by @GitSpoon\n* Updated German translation\n* Fixed widget not updated on login and logout\n* Bug fixes and small improvements\n* Disabled dependency info block in APK signature"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/37.txt",
    "content": "* Fixed a potential crash when navigating back to the search screen\n* Fixed english title is not displayed if only 'en_us' title is available\n* Fixed text overflow for overlay text on media posters\n* Added Italian translations by NicolaBortoletto\n* Updated Spanish translations by gallegonovato and @Kiimby\n* Updated Vietnamese translations by @ngocanhtve"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/38.txt",
    "content": "* Fixed a crash on the library page\n* Added Tamil translation by @TamilNeram\n* Updated Russian translation by @Xapitonov\n* Updated French translation by @GitSpoon\n* Updated Italian translation by NicolaBortoletto\n* Updated dependencies"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/39.txt",
    "content": "Improvements\n* Added the ability to clear start and finish dates for library entries\n* Added followers/following counts with an embedded webview (experimental)\n* Improved update checker\n* General UI improvements\n\nBug Fixes\n* Fixed a crash when opening certain profile links\n* Fixed a crash occurring after restoring app data from backup or performing device-to-device transfer\n\nLocalization\n* Added Portuguese translation by @SantosSi\n* Added Afrikaans translation by @MikeWowzoski\n* Updated Russian translation by @Xapitonov, @stokito and @Danil\n* Updated Turkish translation by @Samprenzo\n* Updated Portuguese (Brazil) translation by Lucas\n* Updated Tamil translation by TamilNeram\n* Updated Spanish translation by Kimby\n* Updated French translation by Thibault and @ehainry\n* Updated German translation"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "Kitsune is an unofficial android app for Kitsu.app (Kitsu.io). It allows you to browse anime & manga and track your progress.\n\nFeatures:\n* Explore and search anime and manga, even without an account\n* View anime and manga details including episodes/chapters and characters\n* Manage your anime & manga library\n* Update your Kitsu profile and account settings\n* Material 3 Design\n* Home screen widget\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "Discover new anime & manga and manage your Kitsu library."
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "Kitsune"
  },
  {
    "path": "fastlane/metadata/android/en-US/video.txt",
    "content": ""
  },
  {
    "path": "fastlane/metadata/android/zh-CN/full_description.txt",
    "content": "Kitsune 是 Kitsu.app (Kitsu.io) 的一个非官方安卓应用程序。它允许您浏览动漫和漫画，并跟踪您的进度。\n\n特点：\n*探索和搜索动漫和漫画，即使不登录帐户\n*查看动画和漫画的详情，包括情节、章节和角色\n*管理您的动漫和漫画库\n*更新您的 Kitsu 个人资料和帐户设置\n*Material 3 设计\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/short_description.txt",
    "content": "探索新的动漫和漫画，并管理你的 Kitsu 库。\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/title.txt",
    "content": "Kitsune\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\naboutLibraries = \"11.1.4\"\naccompanist = \"0.36.0\"\nagp = \"9.1.0\"\nalgoliaInstantSearch = \"3.1.3\"\nandroidxActivity = \"1.11.0\"\nandroidxFragment = \"1.8.9\"\nandroidxGlance = \"1.1.1\"\nandroidxLifecycle = \"2.9.3\"\nandroidxNavigation = \"2.9.7\"\nandroidxPaging = \"3.3.6\"\nandroidxRoom = \"2.8.4\"\nandroidxSecurityCrypto = \"1.1.0-alpha06\"\nandroidxViewPager2 = \"1.1.0\"\nandroidxWorkManager = \"2.11.1\"\nappCompat = \"1.7.1\"\narchUnit = \"1.4.1\"\nassertj = \"3.27.7\"\ncircleImageView = \"3.1.0\"\ncomposeBom = \"2025.09.00\"\nconstraintLayout = \"2.2.1\"\ncoreKtx = \"1.17.0\"\ncoreSplashScreen = \"1.0.1\"\ndataFaker = \"2.0.1\"\nespressoCore = \"3.7.0\"\nexpandableTextView = \"1.0.5\"\nglide = \"5.0.5\"\nglide-compose = \"1.0.0-beta08\"\nglideTransformations = \"4.3.0\"\nhauler = \"5.0.0\"\njackson = \"2.18.2\"\njsonApi = \"0.15\"\njunit = \"4.13.2\"\njunitVersion = \"1.3.0\"\nkoin = \"4.1.1\"\nkotlin = \"2.2.20\"\nkotlinxCoroutines = \"1.10.2\"\nkotlinxSerialization = \"1.9.0\"\nkotpref = \"2.13.2\"\nksp = \"2.3.6\"\nktorClient = \"1.6.8\"\nleakCanary = \"2.14\"\nmaterial = \"1.13.0\"\nmaterialRatingBar = \"1.4.0\"\nmockito = \"6.2.3\"\nmpAndroidChart = \"v3.1.0\"\nokHttp = \"5.3.2\"\nphotoView = \"2.3.0\"\npreferenceKtx = \"1.2.1\"\nretrofit = \"3.0.0\"\nrobolectric = \"4.16.1\"\nscreengrab = \"2.1.1\"\nswiperefreshlayout = \"1.2.0\"\ntestRules = \"1.7.0\"\ntreeView = \"1.2.9\"\n\n[libraries]\naccompanist-themeadapter-material3 = { module = \"com.google.accompanist:accompanist-themeadapter-material3\", version.ref = \"accompanist\"}\naccompanist-permissions = { module = \"com.google.accompanist:accompanist-permissions\", version.ref = \"accompanist\"}\nalgolia-instantsearch-android = { module = \"com.algolia:instantsearch-android\", version.ref = \"algoliaInstantSearch\" }\nalgolia-instantsearch-android-paging3 = { module = \"com.algolia:instantsearch-android-paging3\", version.ref = \"algoliaInstantSearch\" }\nalgolia-instantsearch-coroutines = { module = \"com.algolia:instantsearch-coroutines-extensions\", version.ref = \"algoliaInstantSearch\" }\nandroid-gradle-api = { module = \"com.android.tools.build:gradle-api\", version.ref = \"agp\" }\nandroidx-appcompat = { module = \"androidx.appcompat:appcompat\", version.ref = \"appCompat\" }\nandroidx-activity-compose = { module = \"androidx.activity:activity-compose\", version.ref = \"androidxActivity\" }\nandroidx-compose-bom = { module = \"androidx.compose:compose-bom\", version.ref = \"composeBom\" }\nandroidx-compose-material3 = { module = \"androidx.compose.material3:material3\" }\nandroidx-compose-material3-adaptive = { module = \"androidx.compose.material3.adaptive:adaptive\" }\nandroidx-compose-ui-test = { module = \"androidx.compose.ui:ui-test-junit4\" }\nandroidx-compose-ui-test-manifest = { module = \"androidx.compose.ui:ui-test-manifest\" }\nandroidx-compose-ui-tooling = { module = \"androidx.compose.ui:ui-tooling-preview\" }\nandroidx-compose-ui-tooling-preview = { module = \"androidx.compose.ui:ui-tooling\" }\nandroidx-constraint-layout = { module = \"androidx.constraintlayout:constraintlayout\", version.ref = \"constraintLayout\" }\nandroidx-core-ktx = { module = \"androidx.core:core-ktx\", version.ref = \"coreKtx\" }\nandroidx-core-splashscreen = { module = \"androidx.core:core-splashscreen\", version.ref = \"coreSplashScreen\" }\nandroidx-espresso-contrib = { module = \"androidx.test.espresso:espresso-contrib\", version.ref = \"espressoCore\" }\nandroidx-espresso-core = { module = \"androidx.test.espresso:espresso-core\", version.ref = \"espressoCore\" }\nandroidx-fragment-ktx = { module = \"androidx.fragment:fragment-ktx\", version.ref = \"androidxFragment\" }\nandroidx-glance-appwidget = { module = \"androidx.glance:glance-appwidget\", version.ref = \"androidxGlance\" }\nandroidx-glance-material3 = { module = \"androidx.glance:glance-material3\", version.ref = \"androidxGlance\" }\nandroidx-glance-preview = { module = \"androidx.glance:glance-preview\", version.ref = \"androidxGlance\" }\nandroidx-junit = { module = \"androidx.test.ext:junit\", version.ref = \"junitVersion\" }\nandroidx-junit-ktx = { module = \"androidx.test.ext:junit-ktx\", version.ref = \"junitVersion\" }\nandroidx-lifecycle-livedata-ktx = { module = \"androidx.lifecycle:lifecycle-livedata-ktx\", version.ref = \"androidxLifecycle\" }\nandroidx-lifecycle-viewmodel-ktx = { module = \"androidx.lifecycle:lifecycle-viewmodel-ktx\", version.ref = \"androidxLifecycle\" }\nandroidx-navigation-fragment-ktx = { module = \"androidx.navigation:navigation-fragment-ktx\", version.ref = \"androidxNavigation\" }\nandroidx-navigation-ui-ktx = { module = \"androidx.navigation:navigation-ui-ktx\", version.ref = \"androidxNavigation\" }\nandroidx-paging-runtime-ktx = { module = \"androidx.paging:paging-runtime-ktx\", version.ref = \"androidxPaging\" }\nandroidx-preference-ktx = { module = \"androidx.preference:preference-ktx\", version.ref = \"preferenceKtx\" }\nandroidx-room-compiler = { module = \"androidx.room:room-compiler\", version.ref = \"androidxRoom\" }\nandroidx-room-ktx = { module = \"androidx.room:room-ktx\", version.ref = \"androidxRoom\" }\nandroidx-room-paging = { module = \"androidx.room:room-paging\", version.ref = \"androidxRoom\" }\nandroidx-room-runtime = { module = \"androidx.room:room-runtime\", version.ref = \"androidxRoom\" }\nandroidx-security-crypto = { module = \"androidx.security:security-crypto\", version.ref = \"androidxSecurityCrypto\" }\nandroidx-swiperefreshlayout = { module = \"androidx.swiperefreshlayout:swiperefreshlayout\", version.ref = \"swiperefreshlayout\" }\nandroidx-test-rules = { module = \"androidx.test:rules\", version.ref = \"testRules\" }\nandroidx-viewpager2 = { module = \"androidx.viewpager2:viewpager2\", version.ref = \"androidxViewPager2\" }\nandroidx-workmanager = { module = \"androidx.work:work-runtime-ktx\", version.ref = \"androidxWorkManager\" }\nassertj-core = { module = \"org.assertj:assertj-core\", version.ref = \"assertj\" }\nblogc-expandabletextview = { module = \"at.blogc:expandabletextview\", version.ref = \"expandableTextView\" }\nbmelnychuk-treeview = { module = \"com.github.bmelnychuk:atv\", version.ref = \"treeView\" }\nbumptech-glide = { module = \"com.github.bumptech.glide:glide\", version.ref = \"glide\" }\nbumptech-glide-compose = { module = \"com.github.bumptech.glide:compose\", version.ref = \"glide-compose\" }\nbumptech-glide-ksp = { module = \"com.github.bumptech.glide:ksp\", version.ref = \"glide\" }\nbumptech-glide-okhttp3 = { module = \"com.github.bumptech.glide:okhttp3-integration\", version.ref = \"glide\" }\nchibatching-kotpref = { module = \"com.chibatching.kotpref:kotpref\", version.ref = \"kotpref\" }\nchibatching-kotpref-enum = { module = \"com.chibatching.kotpref:enum-support\", version.ref = \"kotpref\" }\nchibatching-kotpref-livedata = { module = \"com.chibatching.kotpref:livedata-support\", version.ref = \"kotpref\" }\nchrisbanes-photoview = { module = \"com.github.chrisbanes:PhotoView\", version.ref = \"photoView\" }\ndatafaker = { module = \"net.datafaker:datafaker\", version.ref = \"dataFaker\" }\nfasterxml-jackson-databind = { module = \"com.fasterxml.jackson.core:jackson-databind\", version.ref = \"jackson\" }\nfasterxml-jackson-kotlin = { module = \"com.fasterxml.jackson.module:jackson-module-kotlin\", version.ref = \"jackson\" }\nfastlane-screengrab = { module = \"tools.fastlane:screengrab\", version.ref = \"screengrab\" }\nfutured-hauler = { module = \"app.futured.hauler:hauler\", version.ref = \"hauler\" }\nfutured-hauler-databinding = { module = \"app.futured.hauler:databinding\", version.ref = \"hauler\" }\ngoogle-android-material = { module = \"com.google.android.material:material\", version.ref = \"material\" }\nhdodenhof-circleimageview = { module = \"de.hdodenhof:circleimageview\", version.ref = \"circleImageView\" }\ninsert-koin-android = { module = \"io.insert-koin:koin-android\", version.ref = \"koin\" }\ninsert-koin-androidx-navigation = { module = \"io.insert-koin:koin-androidx-navigation\", version.ref = \"koin\" }\ninsert-koin-test-junit4 = { module = \"io.insert-koin:koin-test-junit4\", version.ref = \"koin\" }\njasminb-jsonapi = { module = \"com.github.jasminb:jsonapi-converter\", version.ref = \"jsonApi\" }\njetbrains-kotlinx-coroutines-android = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-android\", version.ref = \"kotlinxCoroutines\" }\njetbrains-kotlinx-coroutines-core = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"kotlinxCoroutines\" }\njetbrains-kotlinx-coroutines-test = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-test\", version.ref = \"kotlinxCoroutines\" }\njetbrains-kotlinx-serialization = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-json\", version.ref = \"kotlinxSerialization\" }\njunit = { module = \"junit:junit\", version.ref = \"junit\" }\nktor-client-okhttp = { module = \"io.ktor:ktor-client-okhttp\", version.ref = \"ktorClient\" }\nmikepenz-aboutlibraries = { module = \"com.mikepenz:aboutlibraries\", version.ref = \"aboutLibraries\" }\nmikepenz-aboutlibraries-core = { module = \"com.mikepenz:aboutlibraries-core\", version.ref = \"aboutLibraries\" }\nmockito-kotlin = { module = \"org.mockito.kotlin:mockito-kotlin\", version.ref = \"mockito\" }\nphiljay-mpandroidchart = { module = \"com.github.PhilJay:MPAndroidChart\", version.ref = \"mpAndroidChart\" }\nrobolectric = { module = \"org.robolectric:robolectric\", version.ref = \"robolectric\" }\nsquareup-leakcanary = { module = \"com.squareup.leakcanary:leakcanary-android\", version.ref = \"leakCanary\" }\nsquareup-okhttp3-logging = { module = \"com.squareup.okhttp3:logging-interceptor\", version.ref = \"okHttp\" }\nsquareup-okhttp3-okhttp = { module = \"com.squareup.okhttp3:okhttp\", version.ref = \"okHttp\" }\nsquareup-retrofit2-jackson = { module = \"com.squareup.retrofit2:converter-jackson\", version.ref = \"retrofit\" }\nsquareup-retrofit2-retrofit = { module = \"com.squareup.retrofit2:retrofit\", version.ref = \"retrofit\" }\ntngtech-archunit-junit4 = { module = \"com.tngtech.archunit:archunit-junit4\", version.ref = \"archUnit\" }\nwasabeef-glide-transformations = { module = \"jp.wasabeef:glide-transformations\", version.ref = \"glideTransformations\" }\nzhanghai-materialratingbar = { module = \"me.zhanghai.android.materialratingbar:library\", version.ref = \"materialRatingBar\" }\n\n[plugins]\naboutlibraries-plugin = { id = \"com.mikepenz.aboutlibraries.plugin\", version.ref = \"aboutLibraries\" }\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\nandroid-legacy-kapt = { id = \"com.android.legacy-kapt\", version.ref = \"agp\" }\nandroidx-navigation-safeargs = { id = \"androidx.navigation.safeargs.kotlin\", version.ref = \"androidxNavigation\" }\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\njetbrains-kotlin-jvm = { id = \"org.jetbrains.kotlin.jvm\", version.ref = \"kotlin\" }\njetbrains-kotlin-parcelize = { id = \"org.jetbrains.kotlin.plugin.parcelize\", version.ref = \"kotlin\" }\njetbrains-kotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\nksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Sep 10 13:42:19 CEST 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.3.1-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "## For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n#\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx1024m -XX:MaxPermSize=256m\n# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n#\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n#Sat Sep 23 11:27:20 CEST 2023\nandroid.enableJetifier=true\nandroid.jetifier.ignorelist=jackson-core\nandroid.nonFinalResIds=false\nandroid.nonTransitiveRClass=false\nandroid.useAndroidX=true\nkotlin.code.style=official\norg.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\\=\"-Xmx1024M\" -Dfile.encoding\\=UTF-8\norg.gradle.unsafe.configuration-cache=true\nandroid.enableR8.fullMode=false\nandroid.usesSdkInManifest.disallowed=false\nandroid.r8.strictFullModeForKeepRules=false\nandroid.r8.optimizedResourceShrinking=false\nandroid.onlyEnableUnitTestForTheTestedBuildType=false\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or 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\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\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# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\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    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "plugin/.gitignore",
    "content": "/build"
  },
  {
    "path": "plugin/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(\"java-gradle-plugin\")\n    alias(libs.plugins.jetbrains.kotlin.jvm)\n}\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_11\n    targetCompatibility = JavaVersion.VERSION_11\n}\n\nkotlin {\n    compilerOptions {\n        jvmTarget = JvmTarget.JVM_11\n    }\n}\n\ndependencies {\n    compileOnly(gradleApi())\n    compileOnly(libs.android.gradle.api)\n    implementation(gradleKotlinDsl())\n}\n\ngradlePlugin {\n    plugins.register(\"kitsune-plugin\") {\n        id = \"kitsune-plugin\"\n        implementationClass = \"io.github.drumber.plugin.CustomPlugin\"\n    }\n}\n"
  },
  {
    "path": "plugin/settings.gradle.kts",
    "content": "rootProject.name = \"plugin\"\n\npluginManagement {\n    repositories {\n        gradlePluginPortal()\n        google()\n        mavenCentral()\n    }\n}\n\ndependencyResolutionManagement {\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    versionCatalogs {\n        create(\"libs\") {\n            from(files(\"../gradle/libs.versions.toml\"))\n        }\n    }\n}\n"
  },
  {
    "path": "plugin/src/main/java/io/github/drumber/plugin/CustomPlugin.kt",
    "content": "package io.github.drumber.plugin\n\nimport com.android.build.api.variant.ApplicationAndroidComponentsExtension\nimport com.android.build.gradle.AppPlugin\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\n\nclass CustomPlugin : Plugin<Project> {\n\n    companion object {\n        const val DEFAULT_PACKAGE = \"io.github.drumber.kitsune\"\n    }\n\n    override fun apply(project: Project) {\n        project.plugins.withType(AppPlugin::class.java) {\n            val androidComponents =\n                project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)\n\n            androidComponents.onVariants { variant ->\n                // extract locales task\n                val langCodesProvider = project.tasks.register(\n                    \"${variant.name}ExtractLocales\",\n                    ExtractLocalesTask::class.java\n                ) { task ->\n                    variant.sources.res?.let { resFiles ->\n                        task.packageName.set(variant.namespace)\n                        task.inputFile.setFrom(resFiles.all)\n                        task.outputDir.set(project.layout.buildDirectory.dir(\"generated/source/appLocales/${variant.name}\"))\n                    }\n                }\n                variant.sources.java?.addGeneratedSourceDirectory(\n                    langCodesProvider,\n                    ExtractLocalesTask::outputDir\n                )\n\n                // replace shortcuts package task\n                val replacePackageInShortcutsProvider = project.tasks.register(\n                    \"${variant.name}ReplaceShortcutsPackage\",\n                    ReplaceShortcutsPackageTask::class.java\n                ) { task ->\n                    variant.sources.res?.let { resFiles ->\n                        task.packageName.set(variant.applicationId)\n                        task.inputFiles.setFrom(resFiles.static)\n                        task.outputDir.set(project.layout.buildDirectory.dir(\"generated/res/shortcuts/${variant.name}\"))\n                    }\n                }\n                variant.sources.res?.addGeneratedSourceDirectory(\n                    replacePackageInShortcutsProvider,\n                    ReplaceShortcutsPackageTask::outputDir\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "plugin/src/main/java/io/github/drumber/plugin/ExtractLocalesTask.kt",
    "content": "package io.github.drumber.plugin\n\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.file.ConfigurableFileCollection\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputFiles\nimport org.gradle.api.tasks.OutputDirectory\nimport org.gradle.api.tasks.TaskAction\nimport java.io.File\n\nabstract class ExtractLocalesTask : DefaultTask() {\n\n    companion object {\n        const val DEFAULT_LANG_CODE = \"en-US\"\n        const val CLASS_NAME = \"AppLocales\"\n    }\n\n    @get:Input\n    abstract val packageName: Property<String>\n\n    @get:InputFiles\n    abstract val inputFile: ConfigurableFileCollection\n\n    @get:OutputDirectory\n    abstract val outputDir: DirectoryProperty\n\n    init {\n        packageName.convention(CustomPlugin.DEFAULT_PACKAGE)\n    }\n\n    @TaskAction\n    fun taskAction() {\n        // extract language codes from 'values' directories containing strings\n        val languageCodes = inputFile.asFileTree\n            .matching { it.include(\"**/values-*/strings.xml\") }\n            .mapNotNull { it.parentFile }\n            .map { it.name.substringAfter(\"values-\") }\n            .mapNotNull { it.resourceQualifierToLanguageCode() }\n            .sorted()\n\n        val appLanguages = listOf(DEFAULT_LANG_CODE) + languageCodes\n\n        // generate java class containing a string array with all supported app languages\n        val outputFile = File(outputDir.get().asFile, \"${packageName.get().replace('.', '/')}/$CLASS_NAME.java\")\n        outputFile.parentFile.mkdirs()\n        outputFile.writeText(\n            \"\"\"\n            package ${packageName.get()};\n            public class $CLASS_NAME {\n                public static final String[] SUPPORTED_LOCALES = {${\n                appLanguages.joinToString(\n                    prefix = \"\\\"\",\n                    separator = \"\\\", \\\"\",\n                    postfix = \"\\\"\"\n                )\n            }};\n            }\n        \"\"\".trimIndent()\n        )\n    }\n\n    private fun String.resourceQualifierToLanguageCode(): String? {\n        val segments = split(\"-\").toMutableList()\n        if (segments.isEmpty()) return null\n        if (segments.size == 1) return segments.first()\n        if (segments[1].startsWith('r')) {\n            segments[1] = segments[1].substring(1)\n        }\n        return segments.joinToString(\"-\")\n    }\n\n}"
  },
  {
    "path": "plugin/src/main/java/io/github/drumber/plugin/ReplaceShortcutsPackageTask.kt",
    "content": "package io.github.drumber.plugin\n\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.file.ConfigurableFileCollection\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputFiles\nimport org.gradle.api.tasks.OutputDirectory\nimport org.gradle.api.tasks.TaskAction\nimport java.io.File\n\nabstract class ReplaceShortcutsPackageTask : DefaultTask() {\n\n    @get:Input\n    abstract val packageName: Property<String>\n\n    @get:InputFiles\n    abstract val inputFiles: ConfigurableFileCollection\n\n    @get:OutputDirectory\n    abstract val outputDir: DirectoryProperty\n\n    @TaskAction\n    fun taskAction() {\n        val packageNameValue = packageName.get()\n\n        val shortcutsFile = inputFiles.asFileTree\n            .matching { it.include(\"**/shortcuts.xml\") }\n            .singleFile\n\n        val content = shortcutsFile.readText()\n        val updatedContent = content.replace(\n            \"android:targetPackage=\\\"${CustomPlugin.DEFAULT_PACKAGE}\\\"\",\n            \"android:targetPackage=\\\"$packageNameValue\\\"\"\n        )\n\n        val outputFile = File(outputDir.get().asFile, \"xml-v25/shortcuts.xml\")\n        outputFile.parentFile.mkdirs()\n        outputFile.writeText(updatedContent)\n    }\n}"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    includeBuild(\"plugin\")\n    repositories {\n        gradlePluginPortal()\n        google()\n        mavenCentral()\n    }\n}\n\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        maven(url = \"https://jitpack.io\")\n        maven(url = \"https://plugins.gradle.org/m2/\")\n    }\n}\n\nrootProject.name = \"Kitsune\"\ninclude(\":app\")\n"
  }
]