[
  {
    "path": ".github/ISSUE_TEMPLATE/bug-反馈.md",
    "content": "---\nname: Bug 反馈\nabout: 描述你所遇到的bug\ntitle: ''\nlabels: 问题反馈\nassignees: guozhigq\n\n---\n\n### 问题描述\n请提供一个清晰而简明的问题描述。\n\n### 复现步骤\n请提供复现该问题所需的具体步骤。\n\n### 预期行为\n请描述你期望的正确行为或结果。\n\n### 系统信息\n请提供关于您的环境的详细信息，包括操作系统、浏览器版本等。\n\n### 相关截图或日志\n如果有的话，请提供相关的截图、错误日志或其他有助于解决问题的信息。\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/功能请求.md",
    "content": "---\nname: 功能请求\nabout: 对于功能的一些建议\ntitle: ''\nlabels: 功能\nassignees: guozhigq\n\n---\n\n### 功能描述\n请提供对所请求功能的清晰描述。\n\n### 目标\n请描述你希望通过这个功能实现的目标。\n\n### 解决方案\n如果你有任何关于如何实现这个功能的想法或建议，请在这里提供。\n\n### 其他\n请提供已实现该功能或类似功能的应用\n"
  },
  {
    "path": ".github/workflows/beta_ci.yml",
    "content": "name: Pilipala Beta\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - \"x-main\"\n    paths-ignore:\n      - \"**.md\"\n      - \"**.txt\"\n      - \".github/**\"\n      - \".idea/**\"\n      - \"!.github/workflows/**\"\n\njobs:\n  update_version:\n    name: Read and update version\n    runs-on: ubuntu-latest\n\n    outputs:\n      # 定义输出变量 version，以便在其他job中引用\n      new_version: ${{ steps.version.outputs.new_version }}\n      last_commit: ${{ steps.get-last-commit.outputs.last_commit }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.ref_name }}\n          fetch-depth: 0\n\n      - name: 获取first parent commit次数\n        id: get-first-parent-commit-count\n        run: |\n          version=$(yq e .version pubspec.yaml | cut -d \"+\" -f 1)\n          recent_release_tag=$(git tag -l | grep $version | egrep -v \"[-|+]\" || true)\n          if [[ \"x$recent_release_tag\" == \"x\" ]]; then\n            echo \"当前版本tag不存在，请手动生成tag.\"\n            exit 1\n          fi\n          git log --oneline --first-parent $recent_release_tag..HEAD\n          first_parent_commit_count=$(git rev-list --first-parent --count $recent_release_tag..HEAD)\n          echo \"count=$first_parent_commit_count\" >> $GITHUB_OUTPUT\n\n      - name: 获取最后一次提交\n        id: get-last-commit\n        run: |\n          last_commit=$(git log -1 --pretty=\"%h %s\" --first-parent)\n          echo \"last_commit=$last_commit\" >> $GITHUB_OUTPUT\n\n      - name: 更新版本号\n        id: version\n        run: |\n          # 读取版本号\n          VERSION=$(yq e .version pubspec.yaml | cut -d \"+\" -f 1)\n\n          # 获取GitHub Actions的run_number\n          #RUN_NUMBER=${{ github.run_number }}\n\n          # 构建新版本号\n          NEW_VERSION=$VERSION-beta.${{ steps.get-first-parent-commit-count.outputs.count }}\n\n          # 输出新版本号\n          echo \"New version: $NEW_VERSION\"\n\n          # 设置新版本号为输出变量\n          echo \"new_version=$NEW_VERSION\" >>$GITHUB_OUTPUT\n\n  android:\n    name: Build CI (Android)\n    needs: update_version\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.ref_name }}\n\n      - name: 构建Java环境\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n          token: ${{secrets.GIT_TOKEN}}\n\n      - name: 检查缓存\n        uses: actions/cache@v2\n        id: cache-flutter\n        with:\n          path: /root/flutter-sdk\n          key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}\n\n      - name: 安装Flutter\n        if: steps.cache-flutter.outputs.cache-hit != 'true'\n        uses: subosito/flutter-action@v2\n        with:\n          flutter-version: 3.19.6\n          channel: any\n\n      - name: 下载项目依赖\n        run: flutter pub get\n\n      - name: 解码生成 jks\n        run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks\n        env:\n          KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}\n\n      - name: 更新版本号\n        id: version\n        run: |\n          # 更新pubspec.yaml文件中的版本号\n          sed -i \"s/version: .*+/version: ${{ needs.update_version.outputs.new_version }}+/g\" pubspec.yaml\n\n      - name: flutter build apk\n        run: flutter build apk --release --split-per-abi\n        env:\n          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}\n          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}\n          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}\n\n      - name: 重命名应用\n        run: |\n          for file in build/app/outputs/flutter-apk/app-*.apk; do\n            if [[ $file =~ app-(.?*)release.apk ]]; then\n              new_file_name=\"build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}v${{ needs.update_version.outputs.new_version }}.apk\"\n              mv \"$file\" \"$new_file_name\"\n            fi\n          done\n\n      - name: 上传\n        uses: actions/upload-artifact@v3\n        with:\n          name: Pilipala-Beta\n          path: |\n            build/app/outputs/flutter-apk/Pili-*.apk\n\n  iOS:\n    name: Build CI (iOS)\n    needs: update_version\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.ref_name }}\n\n      - name: 安装Flutter\n        if: steps.cache-flutter.outputs.cache-hit != 'true'\n        uses: subosito/flutter-action@v2.10.0\n        with:\n          cache: true\n          flutter-version: 3.16.5\n\n      - name: 更新版本号\n        id: version\n        run: |\n          # 更新pubspec.yaml文件中的版本号\n          sed -i \"\" \"s/version: .*+/version: ${{ needs.update_version.outputs.new_version }}+/g\" pubspec.yaml\n\n      - name: flutter build ipa\n        run: |\n          flutter build ios --release --no-codesign \n          ln -sf ./build/ios/iphoneos Payload\n          zip -r9 app.ipa Payload/runner.app\n\n      - name: 重命名应用\n        run: |\n          DATE=${{ steps.date.outputs.date }}\n          for file in app.ipa; do\n            new_file_name=\"build/Pili-v${{ needs.update_version.outputs.new_version }}.ipa\"\n            mv \"$file\" \"$new_file_name\"\n          done\n\n      - name: 上传\n        uses: actions/upload-artifact@v3\n        with:\n          if-no-files-found: error\n          name: Pilipala-Beta\n          path: |\n            build/Pili-*.ipa\n\n  upload:\n    runs-on: ubuntu-latest\n\n    needs:\n      - update_version\n      - android\n      - iOS\n    steps:\n      - uses: actions/download-artifact@v3\n        with:\n          name: Pilipala-Beta\n          path: ./Pilipala-Beta\n\n      - name: 发送到Telegram频道\n        uses: xireiki/channel-post@v1.0.7\n        with:\n          bot_token: ${{ secrets.BOT_TOKEN }}\n          chat_id: ${{ secrets.CHAT_ID }}\n          large_file: true\n          api_id: ${{ secrets.TELEGRAM_API_ID }}\n          api_hash: ${{ secrets.TELEGRAM_API_HASH }}\n          method: sendFile\n          path: Pilipala-Beta/*\n          parse_mode: Markdown\n          context: \"*Beta版本: v${{ needs.update_version.outputs.new_version }}*\\n更新内容: [${{ needs.update_version.outputs.last_commit }}]\"\n"
  },
  {
    "path": ".github/workflows/release_ci.yml",
    "content": "name: Pilipala Release\n\n# action事件触发\non:\n  push:\n    # push tag时触发\n    tags:\n      - \"v*.*.*\"\n\n# 可以有多个jobs\njobs:\n  android:\n    # 运行环境 ubuntu-latest window-latest mac-latest\n    runs-on: ubuntu-latest\n\n    # 每个jobs中可以有多个steps\n    steps:\n      - name: 代码迁出\n        uses: actions/checkout@v3\n\n      - name: 构建Java环境\n        uses: actions/setup-java@v3\n        with:\n          distribution: \"zulu\"\n          java-version: \"17\"\n          token: ${{secrets.GIT_TOKEN}}\n\n      - name: 检查缓存\n        uses: actions/cache@v2\n        id: cache-flutter\n        with:\n          path: /root/flutter-sdk # Flutter SDK 的路径\n          key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}\n\n      - name: 安装Flutter\n        if: steps.cache-flutter.outputs.cache-hit != 'true'\n        uses: subosito/flutter-action@v2\n        with:\n          flutter-version: 3.19.6\n          channel: any\n\n      - name: 下载项目依赖\n        run: flutter pub get\n\n      - name: 解码生成 jks\n        run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks\n        env:\n          KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}\n\n      - name: flutter build apk\n        run: flutter build apk --release --split-per-abi\n        env:\n          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}\n          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}\n          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}\n\n      - name: flutter build apk\n        run: flutter build apk --release\n        env:\n          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}\n          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}\n          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}\n\n      - name: 获取版本号\n        id: version\n        run: echo \"version=${GITHUB_REF#refs/tags/v}\" >>$GITHUB_OUTPUT\n\n      # - name: 获取当前日期\n      #   id: date\n      #   run: echo \"date=$(date +'%m%d')\" >>$GITHUB_OUTPUT\n\n      - name: 重命名应用\n        run: |\n          # DATE=${{ steps.date.outputs.date }}\n          for file in build/app/outputs/flutter-apk/app-*.apk; do\n            if [[ $file =~ app-(.?*)release.apk ]]; then\n              new_file_name=\"build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}${{ steps.version.outputs.version }}.apk\"\n              mv \"$file\" \"$new_file_name\"\n            fi\n          done\n\n      - name: 上传\n        uses: actions/upload-artifact@v3\n        with:\n          name: Pilipala-Release\n          path: |\n            build/app/outputs/flutter-apk/Pili-*.apk\n\n  iOS:\n    runs-on: macos-latest\n\n    steps:\n      - name: 代码迁出\n        uses: actions/checkout@v4\n\n      - name: 安装Flutter\n        if: steps.cache-flutter.outputs.cache-hit != 'true'\n        uses: subosito/flutter-action@v2.10.0\n        with:\n          cache: true\n          flutter-version: 3.19.6\n\n      - name: flutter build ipa\n        run: |\n          flutter build ios --release --no-codesign \n          ln -sf ./build/ios/iphoneos Payload\n          zip -r9 app.ipa Payload/runner.app\n\n      - name: 获取版本号\n        id: version\n        run: echo \"version=${GITHUB_REF#refs/tags/v}\" >>$GITHUB_OUTPUT\n\n      - name: 重命名应用\n        run: |\n          DATE=${{ steps.date.outputs.date }}\n          for file in app.ipa; do\n            new_file_name=\"build/Pili-${{ steps.version.outputs.version }}.ipa\"\n            mv \"$file\" \"$new_file_name\"\n          done\n\n      - name: 上传\n        uses: actions/upload-artifact@v3\n        with:\n          if-no-files-found: error\n          name: Pilipala-Release\n          path: |\n            build/Pili-*.ipa\n\n  upload:\n    runs-on: ubuntu-latest\n\n    needs:\n      - android\n      - iOS\n    steps:\n      - uses: actions/download-artifact@v3\n        with:\n          name: Pilipala-Release\n          path: ./Pilipala-Release\n\n      - name: Install dependencies\n        run: sudo apt-get install tree -y\n\n      - name: Get version\n        id: version\n        run: echo \"version=${GITHUB_REF#refs/tags/v}\" >>$GITHUB_OUTPUT\n\n      - name: Upload Release\n        uses: ncipollo/release-action@v1\n        with:\n          name: v${{ steps.version.outputs.version }}\n          token: ${{ secrets.GIT_TOKEN }}\n          omitBodyDuringUpdate: true\n          omitNameDuringUpdate: true\n          omitPrereleaseDuringUpdate: true\n          allowUpdates: true\n          artifacts: Pilipala-Release/*\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter repo-specific\n/bin/cache/\n/bin/internal/bootstrap.bat\n/bin/internal/bootstrap.sh\n/bin/mingit/\n/dev/benchmarks/mega_gallery/\n/dev/bots/.recipe_deps\n/dev/bots/android_tools/\n/dev/devicelab/ABresults*.json\n/dev/docs/doc/\n/dev/docs/api_docs.zip\n/dev/docs/flutter.docs.zip\n/dev/docs/lib/\n/dev/docs/pubspec.yaml\n/dev/integration_tests/**/xcuserdata\n/dev/integration_tests/**/Pods\n/packages/flutter/coverage/\nversion\nanalysis_benchmark.json\n\n# packages file containing multi-root paths\n.packages.generated\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\nflutter_*.png\nlinked_*.ds\nunlinked.ds\nunlinked_spec.ds\n\n# Obfuscation related\napp.*.map.json\n\n# Android related\n**/android/**/gradle-wrapper.jar\n.gradle/\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n**/android/key.properties\n*.jks\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/.last_build_id\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/ephemeral\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# macOS\n**/Flutter/ephemeral/\n**/Pods/\n**/macos/Flutter/GeneratedPluginRegistrant.swift\n**/macos/Flutter/ephemeral\n**/xcuserdata/\n\n# Windows\n**/windows/flutter/generated_plugin_registrant.cc\n**/windows/flutter/generated_plugin_registrant.h\n**/windows/flutter/generated_plugins.cmake\n\n# Linux\n**/linux/flutter/generated_plugin_registrant.cc\n**/linux/flutter/generated_plugin_registrant.h\n**/linux/flutter/generated_plugins.cmake\n\n# Coverage\ncoverage/\n\n# Symbols\napp.*.symbols\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages\n!/dev/ci/**/Gemfile.lock\n!.vscode/settings.json"
  },
  {
    "path": ".metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled.\n\nversion:\n  revision: 4b12645012342076800eb701bcdfe18f87da21cf\n  channel: stable\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n      base_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n    - platform: android\n      create_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n      base_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n    - platform: ios\n      create_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n      base_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n    - platform: linux\n      create_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n      base_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n    - platform: macos\n      create_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n      base_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n    - platform: web\n      create_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n      base_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n    - platform: windows\n      create_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n      base_revision: 4b12645012342076800eb701bcdfe18f87da21cf\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // 使用 IntelliSense 了解相关属性。 \n    // 悬停以查看现有属性的描述。\n    // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"pilipala\",\n            \"request\": \"launch\",\n            \"type\": \"dart\"\n        },\n        {\n            \"name\": \"pilipala (profile mode)\",\n            \"request\": \"launch\",\n            \"type\": \"dart\",\n            \"flutterMode\": \"profile\"\n        },\n        {\n            \"name\": \"pilipala (release mode)\",\n            \"request\": \"launch\",\n            \"type\": \"dart\",\n            \"flutterMode\": \"release\"\n        }\n    ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.formatOnSave\": true,\n    \"[dart]\": {\n        \"editor.formatOnType\": true\n    }\n}"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n    <img width=\"200\" height=\"200\" src=\"https://github.com/guozhigq/pilipala/blob/main/assets/images/logo/logo_android.png\">\n</div>\n\n<div align=\"center\">\n    <h1>PiliPala</h1>\n<div align=\"center\">\n    \n![GitHub repo size](https://img.shields.io/github/repo-size/guozhigq/pilipala) \n![GitHub Repo stars](https://img.shields.io/github/stars/guozhigq/pilipala) \n![GitHub all releases](https://img.shields.io/github/downloads/guozhigq/pilipala/total) \n\n</div>\n    <p>使用 Flutter 开发的 BiliBili 第三方客户端</p>\n    \n<img src=\"https://github.com/guozhigq/pilipala/blob/main/assets/screenshots/510shots_so.png\" width=\"32%\" alt=\"home\" />\n<img src=\"https://github.com/guozhigq/pilipala/blob/main/assets/screenshots/174shots_so.png\" width=\"32%\" alt=\"home\" />\n<img src=\"https://github.com/guozhigq/pilipala/blob/main/assets/screenshots/850shots_so.png\" width=\"32%\" alt=\"home\" />\n<br/>\n<img src=\"https://github.com/guozhigq/pilipala/blob/main/assets/screenshots/main_screen.png\" width=\"96%\" alt=\"home\" />\n<br/>\n</div>\n\n## 开发环境\n\nXcode 13.4 不支持 ```auto_orientation```，请注释相关代码\n\n```bash\n[✓] Flutter (Channel stable, 3.19.6, on macOS 14.1.2 23B92 darwin-arm64, locale\n    zh-Hans-CN)\n[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)\n[✓] Xcode - develop for iOS and macOS (Xcode 15.1)\n[✓] Chrome - develop for the web\n[✓] Android Studio (version 2022.3)\n[✓] VS Code (version 1.87.2)\n[✓] Connected device (3 available)\n[✓] Network resources\n```\n\n## 技术交流\n\nTelegram: [https://t.me/+1DFtqS6usUM5MDNl](https://t.me/+1DFtqS6usUM5MDNl)\n\nTelegram Beta 版本：@PiliPala_Beta\n\nQQ 频道: https://pd.qq.com/s/365esodk3\n\n## 功能\n\n目前着重移动端 (Android、iOS)，暂时没有适配桌面端、Pad 端、手表端等\n\n现有功能及[开发计划](https://github.com/users/guozhigq/projects/5)\n\n- [x] 推荐视频列表 (app 端)\n- [x] 最热视频列表\n- [x] 热门直播\n- [x] 番剧列表\n- [x] 屏蔽黑名单内用户视频\n- [x] 排行榜\n\n- [x] 用户相关\n  - [x] 粉丝、关注用户、拉黑用户查看\n  - [x] 用户主页查看\n  - [x] 关注/取关用户\n  - [ ] 离线缓存\n  - [x] 稍后再看\n  - [x] 观看记录\n  - [x] 我的收藏\n  - [x] 黑名单管理 \n  \n- [x] 动态相关\n  - [x] 全部、投稿、番剧分类查看\n  - [x] 动态评论查看\n  - [x] 动态评论回复功能\n  - [x] 动态未读标记 \n\n- [x] 视频播放相关\n  - [x] 双击快进/快退\n  - [x] 双击播放/暂停\n  - [x] 垂直方向调节亮度/音量\n  - [x] 垂直方向上滑全屏、下滑退出全屏\n  - [x] 水平方向手势快进/快退\n  - [x] 全屏方向设置\n  - [x] 倍速选择/长按 2 倍速\n  - [x] 硬件加速 (视机型而定)\n  - [x] 画质选择 (高清画质未解锁)\n  - [x] 音质选择 (视视频而定)\n  - [x] 解码格式选择 (视视频而定)\n  - [x] 弹幕\n  - [x] 字幕\n  - [x] 记忆播放\n  - [x] 视频比例：高度/宽度适应、填充、包含等\n  - [x] 视频快照\n  - [x] 直播弹幕\n     \n- [x] 搜索相关\n  - [x] 热搜\n  - [x] 搜索历史\n  - [x] 默认搜索词\n  - [x] 投稿、番剧、直播间、用户搜索\n  - [x] 视频搜索排序、按时长筛选\n    \n- [x] 视频详情页相关\n  - [x] 视频选集 (分 p) 切换\n  - [x] 点赞、投币、收藏/取消收藏\n  - [x] 相关视频查看\n  - [x] 评论用户身份标识\n  - [x] 评论 (排序) 查看、二楼评论查看\n  - [x] 主楼、二楼评论/表情回复功能\n  - [x] 评论点赞\n  - [x] 评论笔记图片查看、保存\n\n- [x] 设置相关\n  - [x] 画质、音质、解码方式预设      \n  - [x] 图片质量设定\n  - [x] 主题模式：亮色/暗色/跟随系统\n  - [x] 震动反馈 (可选)\n  - [x] 高帧率\n  - [x] 自动全屏\n- [ ] 等等\n\n## 下载\n\n可以通过右侧 Releases 进行下载或拉取代码到本地进行编译\n\n### 从 F-Droid 安装\n\n<a href=\"https://f-droid.org/packages/com.guozhigq.pilipala\">\n    <img src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on-zh-cn.png\"\n    alt=\"Get it on F-Droid\"\n    height=\"80\">\n</a>\n\n## 声明\n\n此项目 (PiliPala) 是个人为了兴趣而开发, 仅用于学习和测试。\n所用 API 皆从官方网站收集, 不提供任何破解内容。\n\n感谢使用\n\n## 致谢\n\n- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)\n- [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer)\n- [media-kit](https://github.com/media-kit/media-kit)\n- [dio](https://pub.dev/packages/dio)\n- 等等\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\ninclude: package:flutter_lints/flutter.yaml\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at\n  # https://dart-lang.github.io/linter/lints/index.html.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    # avoid_print: false  # Uncomment to disable the `avoid_print` rule\n    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n**/*.keystore\n**/*.jks\n"
  },
  {
    "path": "android/app/build.gradle",
    "content": "def localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterRoot = localProperties.getProperty('flutter.sdk')\nif (flutterRoot == null) {\n    throw new GradleException(\"Flutter SDK not found. Define location with flutter.sdk in the local.properties file.\")\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply from: \"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle\"\n\ndef keystorePropertiesFile = rootProject.file('key.properties')\ndef keystoreProperties = new Properties()\nif (keystorePropertiesFile.exists()) {\n    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))\n}\n\ndef _storeFile = file(System.getenv(\"KEYSTORE\") ?: keystoreProperties[\"storeFile\"] ?: \"vvex.jks\")\ndef _storePassword = System.getenv(\"KEYSTORE_PASSWORD\") ?: keystoreProperties[\"storePassword\"]\ndef _keyAlias = System.getenv(\"KEY_ALIAS\") ?: keystoreProperties[\"keyAlias\"]\ndef _keyPassword = System.getenv(\"KEY_PASSWORD\") ?: keystoreProperties[\"keyPassword\"]\n\nandroid {\n    compileSdkVersion flutter.compileSdkVersion\n    ndkVersion flutter.ndkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n    defaultConfig {\n        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\n        applicationId \"com.guozhigq.pilipala\"\n        // You can update the following values to match your application needs.\n        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.\n        targetSdkVersion flutter.targetSdkVersion\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n        minSdkVersion 21\n        multiDexEnabled true\n    }\n\n    signingConfigs {\n        // 添加签名配置\n        release {\n            // 配置密钥库文件的位置、别名、密码等信息\n            storeFile _storeFile\n            storePassword _storePassword\n            keyAlias _keyAlias\n            keyPassword _keyPassword\n            v1SigningEnabled true\n            v2SigningEnabled true\n        }\n    }\n\n    buildTypes {\n        release {\n            // TODO: Add your own signing config for the release build.\n            // Signing with the debug keys for now, so `flutter run --release` works.\n            signingConfig signingConfigs.release\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n}\n\next.abiCodes = [\"x86_64\": 1, \"armeabi-v7a\": 2, \"arm64-v8a\": 3]\nimport com.android.build.OutputFile\nandroid.applicationVariants.all { variant ->\n  variant.outputs.each { output ->\n    def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))\n    if (abiVersionCode != null) {\n      output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode\n    }\n  }\n}\n"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.guozhigq.pilipala\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.guozhigq.pilipala\">\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.VIEW\" />\n            <category android:name=\"android.intent.category.BROWSABLE\" />\n            <data android:scheme=\"http\" />\n        </intent>\n        <!-- If your app opens https URLs -->\n        <intent>\n            <action android:name=\"android.intent.action.VIEW\" />\n            <category android:name=\"android.intent.category.BROWSABLE\" />\n            <data android:scheme=\"https\" />\n        </intent>\n\n    </queries>\n    <queries>\n        <intent>\n            <action android:name=\n                \"android.support.customtabs.action.CustomTabsService\" />\n        </intent>\n    </queries>\n\n    <queries>\n        <!-- If your app checks for http support -->\n        <intent>\n            <action android:name=\"android.intent.action.VIEW\" />\n            <category android:name=\"android.intent.category.BROWSABLE\" />\n            <data android:scheme=\"http\" />\n        </intent>\n        <intent>\n            <action android:name=\"android.intent.action.VIEW\" />\n            <category android:name=\"android.intent.category.BROWSABLE\" />\n            <data android:scheme=\"https\" />\n        </intent>\n    </queries>\n\n    <application\n        android:label=\"PiliPala\"\n        android:name=\"${applicationName}\"\n        android:icon=\"@mipmap/ic_launcher\"\n        xmlns:tools=\"http://schemas.android.com/tools\"\n        android:enableOnBackInvokedCallback=\"true\"\n        android:allowBackup=\"false\"\n        android:fullBackupContent=\"false\"\n        tools:replace=\"android:allowBackup\">\n        <activity\n            android:name=\"com.guozhigq.pilipala.MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\"\n            android:supportsPictureInPicture=\"true\"\n            android:resizeableActivity=\"true\"\n            >\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.NormalTheme\"\n              android:resource=\"@style/NormalTheme\"\n              />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.intent.action.SEARCH\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n                <data android:scheme=\"bilibili\" android:host=\"forward\" />\n                <data android:scheme=\"bilibili\" android:host=\"comment\"\n                    android:pathPattern=\"/detail/.*/.*/.*\" />\n                <data android:scheme=\"bilibili\" android:host=\"uper\" />\n                <data android:scheme=\"bilibili\" android:host=\"article\"\n                    android:pathPattern=\"/readlist\" />\n                <data android:scheme=\"bilibili\" android:host=\"advertise\" android:path=\"/home\" />\n                <data android:scheme=\"bilibili\" android:host=\"clip\" />\n                <data android:scheme=\"bilibili\" android:host=\"search\" />\n                <data android:scheme=\"bilibili\" android:host=\"stardust-search\" />\n                <data android:scheme=\"bilibili\" android:host=\"music\" />\n                <data android:scheme=\"bilibili\" android:host=\"bangumi\"\n                    android:pathPattern=\"/season.*\" />\n                <data android:scheme=\"bilibili\" android:host=\"bangumi\" android:pathPattern=\"/.*\" />\n                <data android:scheme=\"bilibili\" android:host=\"pictureshow\"\n                    android:pathPrefix=\"/creative_center\" />\n                <data android:scheme=\"bilibili\" android:host=\"cliparea\" />\n                <data android:scheme=\"bilibili\" android:host=\"im\" />\n                <data android:scheme=\"bilibili\" android:host=\"im\" android:path=\"/notifications\" />\n                <data android:scheme=\"bilibili\" android:host=\"following\" />\n                <data android:scheme=\"bilibili\" android:host=\"following\"\n                    android:pathPattern=\"/detail/.*\" />\n                <data android:scheme=\"bilibili\" android:host=\"following\"\n                    android:path=\"/publishInfo/\" />\n                <data android:scheme=\"bilibili\" android:host=\"laser\" android:pathPattern=\"/.*\" />\n                <data android:scheme=\"bilibili\" android:host=\"livearea\" />\n                <data android:scheme=\"bilibili\" android:host=\"live\" />\n                <data android:scheme=\"bilibili\" android:host=\"catalog\" />\n                <data android:scheme=\"bilibili\" android:host=\"browser\" />\n                <data android:scheme=\"bilibili\" android:host=\"user_center\" />\n                <data android:scheme=\"bilibili\" android:host=\"login\" />\n                <data android:scheme=\"bilibili\" android:host=\"space\" />\n                <data android:scheme=\"bilibili\" android:host=\"author\" />\n                <data android:scheme=\"bilibili\" android:host=\"tag\" />\n                <data android:scheme=\"bilibili\" android:host=\"rank\" />\n                <data android:scheme=\"bilibili\" android:host=\"external\" />\n                <data android:scheme=\"bilibili\" android:host=\"blank\" />\n                <data android:scheme=\"bilibili\" android:host=\"home\" />\n                <data android:scheme=\"bilibili\" android:host=\"root\" />\n                <data android:scheme=\"bilibili\" android:host=\"video\" />\n                <data android:scheme=\"bilibili\" android:host=\"story\" />\n                <data android:scheme=\"bilibili\" android:host=\"podcast\" />\n                <data android:scheme=\"bilibili\" android:host=\"search\" />\n                <data android:scheme=\"bilibili\" android:host=\"main\" android:path=\"/favorite\" />\n                <data android:scheme=\"bilibili\" android:host=\"pgc\" android:path=\"/theater/match\" />\n                <data android:scheme=\"bilibili\" android:host=\"pgc\" android:path=\"/theater/square\" />\n                <data android:scheme=\"bilibili\" android:host=\"m.bilibili.com\"\n                    android:path=\"/topic-detail\" />\n                <data android:scheme=\"bilibili\" android:host=\"article\" />\n                <data android:scheme=\"bilibili\" android:host=\"pegasus\"\n                    android:pathPattern=\"/channel/v2/.*\" />\n                <data android:scheme=\"bilibili\" android:host=\"feed\" android:pathPattern=\"/channel\" />\n                <data android:scheme=\"bilibili\" android:host=\"vip\" />\n                <data android:scheme=\"bilibili\" android:host=\"user_center\" android:path=\"/vip\" />\n                <data android:scheme=\"bilibili\" android:host=\"history\" />\n                <data android:scheme=\"bilibili\" android:host=\"charge\" android:path=\"/rank\" />\n                <data android:scheme=\"bilibili\" android:host=\"assistant\" />\n                <data android:scheme=\"bilibili\" android:host=\"assistant\" />\n                <data android:scheme=\"bilibili\" android:host=\"feedback\" />\n                <data android:scheme=\"bilibili\" android:host=\"auth\" android:path=\"/launch\" />\n\n                <data android:scheme=\"http\" android:host=\"live.bilibili.com\"\n                    android:pathPattern=\"/live/.*\" />\n                <data android:scheme=\"https\" android:host=\"live.bilibili.com\"\n                    android:pathPattern=\"/live/.*\" />\n                    <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.tv\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.tv\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.cn\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.cn\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/mobile/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/mobile/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"m.bilibili.com\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"m.bilibili.com\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/story/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/story/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/bangumi/i/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/bangumi/i/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/mobile/bangumi/i/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/mobile/bangumi/i/.*\" />\n                <data android:scheme=\"http\" android:host=\"bangumi.bilibili.com\"\n                    android:pathPattern=\"/.*\" />\n                <data android:scheme=\"https\" android:host=\"bangumi.bilibili.com\"\n                    android:pathPattern=\"/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/bangumi/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/bangumi/.*\" />\n                <data android:scheme=\"http\" android:host=\"m.bilibili.com\"\n                    android:pathPattern=\"/bangumi/.*\" />\n                <data android:scheme=\"https\" android:host=\"m.bilibili.com\"\n                    android:pathPattern=\"/bangumi/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/cheese/play/ss.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/cheese/play/ss.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/cheese/play/ep.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/cheese/play/ep.*\" />\n                <data android:scheme=\"http\" android:host=\"m.bilibili.com\"\n                    android:pathPattern=\"/bangumi/play/ss.*\" />\n                <data android:scheme=\"https\" android:host=\"m.bilibili.com\"\n                    android:pathPattern=\"/cheese/play/ss.*\" />\n                <data android:scheme=\"http\" android:host=\"m.bilibili.com\"\n                    android:pathPattern=\"/cheese/play/ep.*\" />\n                <data android:scheme=\"https\" android:host=\"m.bilibili.com\"\n                    android:pathPattern=\"/cheese/play/ep.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/read/cv.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/read/cv.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\" android:path=\"/review/\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\" android:path=\"/review/\" />\n                <data android:scheme=\"http\" android:host=\"bilibili.cn\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"bilibili.cn\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"bilibili.com\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"bilibili.com\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.cn\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.cn\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/video/.*\" />\n                <data android:scheme=\"http\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/mobile/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"www.bilibili.com\"\n                    android:pathPattern=\"/mobile/video/.*\" />\n                <data android:scheme=\"https\" android:host=\"b23.tv\"\n                    android:pathPattern=\"/*\" />\n                <data android:scheme=\"https\" android:host=\"space.bilibili.com\"\n                    android:pathPattern=\"/*\" />\n\n            </intent-filter>\n        </activity>\n        <service \n            android:name=\"com.ryanheise.audioservice.AudioService\"\n            android:foregroundServiceType=\"mediaPlayback\"\n            android:exported=\"true\" \n            tools:ignore=\"Instantiatable\">\n            <intent-filter>\n                <action android:name=\"android.media.browse.MediaBrowserService\" />\n            </intent-filter>\n        </service>\n\n        <receiver \n            android:name=\"com.ryanheise.audioservice.MediaButtonReceiver\"\n            android:exported=\"true\" \n            tools:ignore=\"Instantiatable\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MEDIA_BUTTON\" />\n            </intent-filter>\n        </receiver> \n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n    </application>\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\"/>\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\"/>\n    <!--\n      Media access permissions.\n      Android 13 or higher.\n      https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions\n      -->\n    <uses-permission android:name=\"android.permission.READ_MEDIA_AUDIO\" />\n    <uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" />\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/guozhigq/pilipala/MainActivity.kt",
    "content": "package com.guozhigq.pilipala\n\n// import io.flutter.embedding.android.FlutterActivity\nimport com.ryanheise.audioservice.AudioServiceActivity;\n\nclass MainActivity: AudioServiceActivity() {\n    \n}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_baseline_forward_10_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#FFFFFF\"\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=\"M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z\"/>\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z\"/>\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_baseline_replay_10_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#FFFFFF\"\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=\"M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z\"/>\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z\"/>\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "android/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>\n"
  },
  {
    "path": "android/app/src/main/res/raw/keep.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\"\n  tools:keep=\"@drawable/*\" />"
  },
  {
    "path": "android/app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#ffffff</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:targetApi=\"o_mr1\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.guozhigq.pilipala\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/build.gradle",
    "content": "buildscript {\n    ext.kotlin_version = '1.9.0'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.2.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n}\nsubprojects {\n    project.evaluationDependsOn(':app')\n}\n\ntasks.register(\"clean\", Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-all.zip\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "include ':app'\n\ndef localPropertiesFile = new File(rootProject.projectDir, \"local.properties\")\ndef properties = new Properties()\n\nassert localPropertiesFile.exists()\nlocalPropertiesFile.withReader(\"UTF-8\") { reader -> properties.load(reader) }\n\ndef flutterSdkPath = properties.getProperty(\"flutter.sdk\")\nassert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\napply from: \"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle\"\n"
  },
  {
    "path": "assets/loading.json",
    "content": "{\"v\":\"5.7.11\",\"fr\":60,\"ip\":0,\"op\":81,\"w\":1920,\"h\":1080,\"nm\":\"Loading Dots\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Dot4\",\"sr\":1,\"ks\":{\"o\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":25,\"s\":[25]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":39,\"s\":[100]},{\"t\":55,\"s\":[25]}],\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":25,\"s\":[1142,540,0],\"to\":[0,-6.667,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":39,\"s\":[1142,500,0],\"to\":[0,0,0],\"ti\":[0,-6.667,0]},{\"t\":55,\"s\":[1142,540,0]}],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[-284,92,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":25,\"s\":[50,50,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":39,\"s\":[75,75,100]},{\"t\":55,\"s\":[50,50,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[120,120],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.0039,0.6157,0.5686,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[-284,92],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":360,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Dot3\",\"sr\":1,\"ks\":{\"o\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":17,\"s\":[25]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":31,\"s\":[100]},{\"t\":47,\"s\":[25]}],\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":17,\"s\":[1022,540,0],\"to\":[0,-6.667,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":31,\"s\":[1022,500,0],\"to\":[0,0,0],\"ti\":[0,-6.667,0]},{\"t\":47,\"s\":[1022,540,0]}],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[-284,92,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":17,\"s\":[50,50,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":31,\"s\":[75,75,100]},{\"t\":47,\"s\":[50,50,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[120,120],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.0039,0.6157,0.5686,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[-284,92],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":360,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Dot2\",\"sr\":1,\"ks\":{\"o\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":9,\"s\":[25]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":23,\"s\":[100]},{\"t\":39,\"s\":[25]}],\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":9,\"s\":[902,540,0],\"to\":[0,-6.667,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":23,\"s\":[902,500,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":39,\"s\":[902,540,0]}],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[-284,92,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":9,\"s\":[50,50,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":23,\"s\":[75,75,100]},{\"t\":39,\"s\":[50,50,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[120,120],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.0039,0.6157,0.5686,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[-284,92],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":360,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Dot1\",\"sr\":1,\"ks\":{\"o\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":0,\"s\":[25]},{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"t\":14,\"s\":[100]},{\"t\":30,\"s\":[25]}],\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":0,\"s\":[782,540,0],\"to\":[0,-6.667,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":14,\"s\":[782,500,0],\"to\":[0,0,0],\"ti\":[0,-6.667,0]},{\"t\":30,\"s\":[782,540,0]}],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[-284,92,0],\"ix\":1,\"l\":2},\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":0,\"s\":[50,50,100]},{\"i\":{\"x\":[0.667,0.667,0.667],\"y\":[1,1,1]},\"o\":{\"x\":[0.333,0.333,0.333],\"y\":[0,0,0]},\"t\":14,\"s\":[75,75,100]},{\"t\":30,\"s\":[50,50,100]}],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[120,120],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.0039,0.6157,0.5686,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"bm\":0,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[-284,92],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":360,\"st\":0,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "assets/trail_loading.json",
    "content": "{\"v\":\"4.6.8\",\"fr\":60,\"ip\":0,\"op\":106,\"w\":500,\"h\":500,\"nm\":\"Comp 1\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"Shape Layer 5\",\"ks\":{\"o\":{\"a\":0,\"k\":100},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"n\":[\"0p667_1_0p333_0\"],\"t\":20,\"s\":[0],\"e\":[360]},{\"t\":110}]},\"p\":{\"a\":0,\"k\":[251,250,0]},\"a\":{\"a\":0,\"k\":[0,0,0]},\"s\":{\"a\":0,\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[10,10]},\"p\":{\"a\":0,\"k\":[0,-100]},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\"},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1]},\"o\":{\"a\":0,\"k\":100},\"w\":{\"a\":0,\"k\":0},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\"},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0.7294118,1,1]},\"o\":{\"a\":0,\"k\":100},\"r\":1,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\"},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\"}],\"ip\":20,\"op\":620,\"st\":20,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Shape Layer 4\",\"ks\":{\"o\":{\"a\":0,\"k\":100},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"n\":[\"0p667_1_0p333_0\"],\"t\":15,\"s\":[0],\"e\":[360]},{\"t\":105}]},\"p\":{\"a\":0,\"k\":[251,250,0]},\"a\":{\"a\":0,\"k\":[0,0,0]},\"s\":{\"a\":0,\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[20,20]},\"p\":{\"a\":0,\"k\":[0,-100]},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\"},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1]},\"o\":{\"a\":0,\"k\":100},\"w\":{\"a\":0,\"k\":0},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\"},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0.7294118,1,1]},\"o\":{\"a\":0,\"k\":100},\"r\":1,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\"},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\"}],\"ip\":15,\"op\":615,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"Shape Layer 3\",\"ks\":{\"o\":{\"a\":0,\"k\":100},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"n\":[\"0p667_1_0p333_0\"],\"t\":10,\"s\":[0],\"e\":[360]},{\"t\":100}]},\"p\":{\"a\":0,\"k\":[251,250,0]},\"a\":{\"a\":0,\"k\":[0,0,0]},\"s\":{\"a\":0,\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[30,30]},\"p\":{\"a\":0,\"k\":[0,-100]},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\"},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1]},\"o\":{\"a\":0,\"k\":100},\"w\":{\"a\":0,\"k\":0},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\"},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0.7294118,1,1]},\"o\":{\"a\":0,\"k\":100},\"r\":1,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\"},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\"}],\"ip\":10,\"op\":610,\"st\":10,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"Shape Layer 2\",\"ks\":{\"o\":{\"a\":0,\"k\":100},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"n\":[\"0p667_1_0p333_0\"],\"t\":5,\"s\":[0],\"e\":[360]},{\"t\":95}]},\"p\":{\"a\":0,\"k\":[251,250,0]},\"a\":{\"a\":0,\"k\":[0,0,0]},\"s\":{\"a\":0,\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[40,40]},\"p\":{\"a\":0,\"k\":[0,-100]},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\"},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1]},\"o\":{\"a\":0,\"k\":100},\"w\":{\"a\":0,\"k\":0},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\"},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0.7294118,1,1]},\"o\":{\"a\":0,\"k\":100},\"r\":1,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\"},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\"}],\"ip\":5,\"op\":605,\"st\":5,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"Shape Layer 1\",\"ks\":{\"o\":{\"a\":0,\"k\":100},\"r\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0]},\"n\":[\"0p667_1_0p333_0\"],\"t\":0,\"s\":[0],\"e\":[360]},{\"t\":90}]},\"p\":{\"a\":0,\"k\":[250,250,0]},\"a\":{\"a\":0,\"k\":[0,0,0]},\"s\":{\"a\":0,\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"n\":[\"0p667_1_0p333_0\",\"0p667_1_0p333_0\"],\"t\":0,\"s\":[50,50],\"e\":[40,40]},{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"n\":[\"0p667_1_0p333_0\",\"0p667_1_0p333_0\"],\"t\":84,\"s\":[40,40],\"e\":[50,50]},{\"t\":100}]},\"p\":{\"a\":0,\"k\":[0,-100]},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\"},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0,0,0,1]},\"o\":{\"a\":0,\"k\":100},\"w\":{\"a\":0,\"k\":0},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\"},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0,0.7294118,1,1]},\"o\":{\"a\":0,\"k\":100},\"r\":1,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector Graphic - Fill\"},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":3,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\"}],\"ip\":0,\"op\":600,\"st\":0,\"bm\":0,\"sr\":1}]}"
  },
  {
    "path": "change_log/1.0.0.0817.md",
    "content": "## 1.0.0\n\n### 初始版本\n+ 直播、推荐、动态功能\n+ 投稿、番剧播放功能\n+ 播放器手势支持\n+ 画质、音质、解码格式支持\n+ 点赞、投币、收藏功能\n+ 关注/取关、用户主页功能\n+ 评论功能\n+ 历史记录、稍后再看功能"
  },
  {
    "path": "change_log/1.0.1.0817.md",
    "content": "## 1.0.1\n\n### 修复\n+ 升级播放器依赖\n+ android平台 AV1格式视频支持\n+ 视频全屏功能\n\n"
  },
  {
    "path": "change_log/1.0.10.1016.md",
    "content": "## 1.0.10\n\n### 修复\n+ 长按倍速抬起后未恢复默认倍速"
  },
  {
    "path": "change_log/1.0.11.1112.md",
    "content": "## 1.0.11\n\n### 新功能\n+ 适配了原生媒体通知栏 @Daydreamer-riri\n+ 视频主题图标 @Daydreamer-riri\n+ 关闭软件后自动画中画播放\n+ UP主分组管理\n+ md2样式底栏\n+ \n\n \n### 修复\n+ 历史记录记忆播放\n+ 部分类型视频连播\n+ 播放速度选择框不支持返回手势\n+ 播放速度选择框不支持返回手势\n+ 视频播放速度总是显示1.0X\n+ 评论页面计数错误\n+ 退出视频还有声音\n\n  \n### 优化\n+ 视频加载速度\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。"
  },
  {
    "path": "change_log/1.0.12.1114.md",
    "content": "## 1.0.12\n\n \n### 修复\n+ iOS端视频播放时没有声音\n+ 超过6分钟弹幕不显示\n+ 视频详情页网络异常\n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。"
  },
  {
    "path": "change_log/1.0.13.1217.md",
    "content": "## 1.0.13\r\n\r\n\r\n### 新功能\r\n+ 视频详情页稍后再看\r\n+ 发送弹幕 感谢@orz12\r\n+ 消息展示\r\n+ up主页显示获赞数\r\n+ up主页显示合集\r\n+ 视频详情页「ai总结」增加开关\r\n  \r\n### 修复\r\n+ 首页推荐问题（需要重新登录）\r\n+ 长按倍速逻辑\r\n+ 视频详情页网络异常\r\n\r\n### 优化\r\n+ 设置面板样式 感谢@GuMengYu @KoolShow\r\n\r\n\r\n更多更新日志可在Github上查看\r\n问题反馈、功能建议请查看「关于」页面。\r\n"
  },
  {
    "path": "change_log/1.0.14.1225.md",
    "content": "## 1.0.14\n\n圣诞节快乐～ 🎉\n\n大部分内容由@orz12提供，感谢👏\n\n### 修复\n+ 全屏弹幕消失\n+ iOS全屏/退出全屏视频暂停\n+ 个人主页关注状态\n+ 视频合集向下滑动UI问题\n+ 媒体库滑动底栏不隐藏\n+ 个人主页动态加载问题 * 2\n+ 未登录状态访问个人主页异常\n+ 视频搜索标题特殊字符转义\n+ iOS闪退\n+ 消息页面夜间模式异常\n+ 消息页面含有撤回消息时异常\n+ 弹幕速度\n\n### 优化\n+ 全屏播放方案优化\n+ 弹幕加载逻辑优化\n+ 点赞、投币逻辑优化\n+ 进度条及播放时间渲染优化\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.15.0101.md",
    "content": "## 1.0.15\n\n元旦快乐～ 🎉\n\n### 功能\n+ 转发动态评论展示\n+ 推荐、最热、收藏视频增肌日期显示\n\n### 修复\n+ 全屏播放相关问题\n+ 评论区@用户展示问题\n+ 登录状态闪退问题\n+ pip意外触发问题\n+ 动态页tab切换样式问题\n\n### 优化\n+ 首页默认使用web端推荐\n+ 取消iOS路由切换效果\n+ 视频分享中添加Up主\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.16.0102.md",
    "content": "## 1.0.16\n\n\n### 功能\n+ toast 背景支持透明度调节\n\n### 修复\n+ web端推荐未展示【已关注】\n+ up主动态页异常\n+ 未打开自动播放时，视频详情页异常\n+ 视频暂停状态取消自动ip\n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.17.0125.md",
    "content": "## 1.0.17\n\n\n### 功能\n+ 视频全屏时隐藏进度条\n+ 动态内容增加投稿跳转\n+ 未开启自动播放时点击封面播放\n+ 弹幕发送标识\n+ 定时关闭\n+ 推荐视频卡片拉黑up功能\n+ 首页tabbar编辑排序\n\n### 修复\n+ 连续跳转搜索页未刷新\n+ 搜索结果为空时页面异常\n+ 评论区链接解析\n+ 视频全屏状态栏背景色\n+ 私信对话气泡位置\n+ 设置up关注分组样式\n+ 每次推荐请求数据相同\n+ iOS代理网络异常\n+ 双击切换播放状态无声\n+ 设置自定义倍速白屏\n+ 免登录查看1080p\n\n### 优化\n+ 首页web端推荐观看数展示\n+ 首页web端推荐接口更新\n+ 首页样式\n+ 搜索页跳转\n+ 弹幕资源优化\n+ 图片渲染占用内存优化（部分）\n+ 两次返回退出应用\n+ schame 补充\n  \n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.18.0130.md",
    "content": "## 1.0.18\n\n\n### 功能\n\n\n### 修复\n\n\n### 优化\n\n  \n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.19.0131.md",
    "content": "## 1.0.19\n\n\n### 修复\n+ 视频404、评论加载错误\n+ bvav转换\n\n### 优化\n+ 视频详情页内存占用\n\n  \n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.2.0819.md",
    "content": "## 1.0.2\n\n### 新功能\n+ 自动检查更新\n+ 封面图片保存\n+ 动态跳转番剧\n+ 历史记录番剧记忆播放\n+ 一键清空稍后再看\n  \n### 修复\n+ 切换分P cid未切换\n+ cookie存储问题\n+ 登录/退出登录问题\n  \n### 优化\n+ 页面空/异常状态样式\n+ 退出登录提示\n+ 请求节流\n+ 全屏播放"
  },
  {
    "path": "change_log/1.0.20.0303.md",
    "content": "## 1.0.20\n\n\n### 功能\n+ 评论区增加表情\n+ 首页渐变背景开关\n+ 媒体库显示「我的订阅」\n+ 评论区链接解析\n+ 默认启动页设置\n\n### 修复\n+ 评论区内容重复\n+ pip相关问题\n+ 播放多p视频评论不刷新\n+ 视频评论翻页重复\n\n### 优化\n+ url scheme优化\n+ 图片预览放大\n+ 图片加载速度\n+ 视频评论区复制\n+ 全屏显示视频标题\n+ 网络异常处理\n\n  \n\n  \n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.21.0306.md",
    "content": "## 1.0.21\n\n### 修复\n+ 推荐视频全屏问题\n+ 番剧全屏播放时灰屏问题\n+ 评论回调导致页面卡死问题\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.22.0430.md",
    "content": "## 1.0.22\n\n### 功能\n+ 字幕\n+ 全屏时选集\n+ 动态转发\n+ 评论视频并转发\n+ 收藏夹删除\n+ 合集显示封面\n+ 底部导航栏编辑、排序功能\n+ 历史记录进度条展示\n+ 直播画质切换\n+ 排行榜功能\n+ 视频详情页推荐视频开关\n+ 显示联合投稿up\n  \n### 修复\n+ 收藏夹个数错误\n+ 封面保存权限问题\n+ 合集最后1p未展示\n+ up主页关注按钮触发灰屏\n\n### 优化\n+ 视频简介查看逻辑\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.23.0504.md",
    "content": "## 1.0.23\n\n### 功能\n+ 封面下载\n\n  \n### 修复\n+ 全屏问题\n+ 视频播放器灰屏问题\n+ 评论区点击区域问题\n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.23.0505.md",
    "content": "## 1.0.23\n\n### 功能\n+ 封面下载\n\n  \n### 修复\n+ 全屏问题\n+ 视频播放器灰屏问题\n+ 评论区点击区域问题\n+ 动态详情跳转异常问题\n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.24.0626.md",
    "content": "## 1.0.24\n\n### 功能\n+ 私信功能\n+ 回复我的、收到的赞查看\n+ 新的登录方式\n+ 全屏选集\n+ 一键三连\n+ 按分区搜索\n\n### 优化\n+ 页面跳转动画\n+ 评论区跳转\n  \n### 修复\n+ 音画不同步问题\n+ 分集字幕未同步\n+ 多语言字幕\n+ 弹幕设置未生效\n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.25.1010.md",
    "content": "## 1.0.25\n\n### 功能\n+ 直播弹幕\n+ 稍后再看、收藏夹播放全部\n+ 收藏夹新建、编辑\n+ 评论删除\n+ 评论保存为图片\n+ 动态页滑动切换up\n+ up投稿筛选充电视频\n+ 直播tab展示关注up\n+ up主页专栏展示\n\n### 优化\n+ 视频详情页一键三连\n+ 动态页标识充电视频\n+ 播放器亮度、音量调整百分比展示\n+ 封面预览时视频标题可复制\n+ 竖屏直播布局\n+ 图片预览\n+ 专栏渲染优化\n+ 私信图片查看\n  \n### 修复\n+ 收藏夹点击异常\n+ 搜索up异常\n+ 系统通知已读异常\n+ [赞了我的]展示错误\n+ 部分up合集无法打开\n+ 切换合集视频投币个数未重置\n+ 搜索条件筛选面板无法滚动\n+ 部分机型导航条未沉浸\n+ 专栏图片渲染问题\n+ 专栏浏览历史记录\n+ 直播间历史记录\n\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.3.0821.md",
    "content": "## 1.0.3\n\n建议卸载1.0.2版本，重新安装\n### 新功能\n+ 底部播放进度条设置\n+ 复制图片链接\n\n  \n### 修复\n+ 用户数据格式修改\n+ video Fit\n+ 没有audio 资源的视频异常\n+ 评论区域图片无法点击\n+ 视频进度条拖动问题\n  \n### 优化\n+ 页面空/异常状态样式\n+ 部分页面样式\n+ 图片预览页面样式"
  },
  {
    "path": "change_log/1.0.4.0822.md",
    "content": "## 1.0.4\n\n### 新功能\n+ 热搜刷新\n+ 视频搜索排序、筛选\n+ app字体大小自定义\n+ app主题色自定义\n+ 「课堂」类动态渲染\n\n  \n### 修复\n+ 搜索词联想richText渲染异常\n+ 部分动态点赞异常\n+ 默认视频解码格式\n+ 搜索页面返回搜索词未清空\n+ 动态详情评论加载异常\n+ 动态页面下拉刷新数据异常\n  \n### 优化\n+ 一些样式修改\n+ 取消热搜词缓存"
  },
  {
    "path": "change_log/1.0.5.0826.md",
    "content": "## 1.0.5\n\n主要是bug修复跟一部分小功能，弹幕功能需要下一版。\n问题反馈请前往QQ频道或提交issues。\n感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」\n\n### 新功能\n+ 高帧率支持\n+ 默认评论排序设置\n+ 默认动态类别设置\n+ 动态合集查看\n+ 同时观看人数\n+ iOS路由切换效果\n\n  \n### 修复\n+ 收藏夹翻页\n+ 首页搜索框频繁点击消失\n+ 评论排序切换空白\n+ 快速返回首页\n+ 重复进入个人中心页面数据未刷新\n+ 动态goods数据异常\n+ 大会员切换番剧\n+ 高画质codes匹配\n\n  \n### 优化\n+ 倍速选择\n+ 播放器亮度记忆\n+ 下载对应版本apk"
  },
  {
    "path": "change_log/1.0.6.0902.md",
    "content": "## 1.0.6\n\n问题反馈、功能建议请查看「关于」页面。\n\n### 新功能\n+ 首页单列布局\n+ 首页推荐展示播放量、弹幕数\n+ 简单弹幕功能实现（持续开发中...）\n+ 评论区搜索关键词开关 issues#46\n+ 热搜榜隐藏功能 issues#35\n+ 自动全屏 issues#37\n+ 快速收藏功能\n+ 双击快进/快退开关\n+ 评论链接跳转视频\n+ 支持移除单个稍后再看\n+ app scheme外链跳转\n\n  \n### 修复\n+ 杜比、无损音频切换\n+ 收藏夹展示 issues#42\n+ 搜索建议次 issues#47\n\n  \n### 优化\n+ 倍速选择优化\n+ 导航条沉浸\n+ 取消Hero动画\n+ 视频锁定逻辑\n+ 登录逻辑优化\n+ 图片预览样式\n+ +评论区用户点击范围\n+ 关注、粉丝页面优化\n+ 关闭自动播放时播放器初始化逻辑"
  },
  {
    "path": "change_log/1.0.7.0908.md",
    "content": "## 1.0.7\n\n默认倍速、直播弹幕、专栏等功能开发中\n\n### 新功能\n+ 弹幕设置、屏蔽功能\n+ 不是很完美的后台播放功能\n+ 不是很完美的画中画(pip)功能（Android端）\n \n### 修复\n+ 动态页面加载异常\n+ 网络异常时页面空白\n+ 竖屏全屏状态栏问题\n+ iOS端代理请求异常\n  \n### 优化\n+ 图片预览\n+ 全屏播放时自动旋转\n+ 转发内容增加视频标题\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.8.0917.md",
    "content": "## 1.0.8\n\n直播弹幕、循环播放等功能开发中\n\n### 新功能\n+ 用户拉黑功能\n+ gif图片保存\n+ 删除已看历史记录\n \n### 修复\n+ 弹幕数量较少\n+ 弹幕屏蔽设置自动记忆\n+ 动态页面渲染\n+ 用户主页数据错乱\n+ 大家都在搜空白\n+ 默认自动全屏，顶部操作栏丢失\n  \n  \n### 优化\n+ 全屏状态栏区域显示优化\n+ 图片保存至PiliPala文件夹\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "change_log/1.0.9.1015.md",
    "content": "## 1.0.9\n\n\n### 新功能\n+ 自定义倍速、默认倍速\n+ 历史记录搜索\n+ 收藏夹搜索\n+ 历史记录多选删除\n+ 视频循环播放\n+ 免登录看1080P\n+ 评论区视频链接跳转\n+ up主分组\n+ up主投稿搜索\n \n### 修复\n+ 搜索视频标题乱码\n+ 屏幕帧率\n+ 动态页面渲染\n\n  \n  \n### 优化\n+ 快进手势\n+ 视频简介链接匹配\n+ 视频全屏时安全区域\n\n更多更新日志可在Github上查看\n问题反馈、功能建议请查看「关于」页面。\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "PiliPala is a third-party Bilibili client developed in Flutter.\n\nTop Features:\n\n* List of recommended videos\n* List of hottest videos\n* Popular live streams\n* List of bangumis\n* Block videos from blacklisted users\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "A third-party Bilibili client developed in Flutter\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "PiliPala\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/changelogs/2001.txt",
    "content": "修复\n\n* 全屏弹幕消失\n* iOS 全屏/退出全屏视频暂停\n* 个人主页关注状态\n* 视频合集向下滑动UI问题\n* 媒体库滑动底栏不隐藏\n* 个人主页动态加载问题 * 2\n* 未登录状态访问个人主页异常\n* 视频搜索标题特殊字符转义\n* iOS 闪退\n* 消息页面夜间模式异常\n* 消息页面含有撤回消息时异常\n* 弹幕速度\n\n优化\n\n* 全屏播放方案优化\n* 弹幕加载逻辑优化\n* 点赞、投币逻辑优化\n* 进度条及播放时间渲染优化\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/full_description.txt",
    "content": "PiliPala 是使用 Flutter 开发的 BiliBili 第三方客户端。\n\n主要功能：\n\n* 推荐视频列表 (app 端)\n* 最热视频列表\n* 热门直播\n* 番剧列表\n* 屏蔽黑名单内用户视频\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/short_description.txt",
    "content": "使用 Flutter 开发的 BiliBili 第三方客户端\n"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/title.txt",
    "content": "PiliPala\n"
  },
  {
    "path": "ios/.gitignore",
    "content": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>12.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Flutter/Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Podfile",
    "content": "# Uncomment this line to define a global platform for your project\nplatform :ios, '13.0'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_ios_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n    target.build_configurations.each do |config|\n      deployment_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']\n      if !deployment_target.nil? && !deployment_target.empty? && deployment_target.to_f < 12.0\n        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\nimport AVFoundation\n\n@UIApplicationMain\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    GeneratedPluginRegistrant.register(with: self)\n    \n    // 设置音频会话类别，确保在静音模式下播放音频\n    do {\n      try AVAudioSession.sharedInstance().setCategory(.playback, options: [.duckOthers])\n    } catch {\n      print(\"Failed to set audio session category: \\(error)\")\n    }\n    \n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"83.5x83.5\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"1024x1024\",\n      \"idiom\" : \"ios-marketing\",\n      \"filename\" : \"Icon-App-1024x1024@1x.png\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"22505\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" colorMatched=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <device id=\"retina6_12\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"22504\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"393\" height=\"852\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"-11\" y=\"-41\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>CFBundleDevelopmentRegion</key>\n\t\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t\t<key>CFBundleDisplayName</key>\n\t\t<string>PiliPala</string>\n\t\t<key>CFBundleExecutable</key>\n\t\t<string>$(EXECUTABLE_NAME)</string>\n\t\t<key>CFBundleIdentifier</key>\n\t\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t\t<key>CFBundleInfoDictionaryVersion</key>\n\t\t<string>6.0</string>\n\t\t<key>CFBundleName</key>\n\t\t<string>PiliPala</string>\n\t\t<key>CFBundlePackageType</key>\n\t\t<string>APPL</string>\n\t\t<key>CFBundleShortVersionString</key>\n\t\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t\t<key>CFBundleSignature</key>\n\t\t<string>????</string>\n\t\t<key>CFBundleVersion</key>\n\t\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t\t<key>LSRequiresIPhoneOS</key>\n\t\t<true/>\n\t\t<key>UILaunchStoryboardName</key>\n\t\t<string>LaunchScreen</string>\n\t\t<key>UIMainStoryboardFile</key>\n\t\t<string>Main</string>\n\t\t<key>UISupportedInterfaceOrientations</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t</array>\n\t\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t</array>\n\t\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t\t<false/>\n\t\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t\t<true/>\n\t\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t\t<true/>\n\t\t<key>NSPhotoLibraryAddUsageDescription</key>\n\t\t<string>请允许APP保存图片到相册</string>\n\t\t<key>NSPhotoLibraryUsageDescription</key>\n\t\t<string>请允许APP保存图片到相册</string>\n\t\t<key>NSCameraUsageDescription</key>\n\t\t<string>App需要您的同意,才能访问相册</string>\n\t\t<key>NSAppleMusicUsageDescription</key>\n\t\t<string>App需要您的同意,才能访问媒体资料库</string>\n\t\t<key>LSApplicationQueriesSchemes</key>\n\t\t<array>\n\t\t\t<string>https</string>\n\t\t\t<string>http</string>\n\t\t</array>\n\t\t<!-- Add Scheme related information -->\n\t\t<key>CFBundleURLTypes</key>\n\t\t<array>\n\t\t\t<dict>\n\t\t\t\t<key>CFBundleURLName</key>\n\t\t\t\t<string></string>\n\t\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>http</string>\n\t\t\t\t\t<string>https</string>\n\t\t\t\t</array>\n\t\t\t\t<key>CFBundleURLTypes</key>\n\t\t\t\t<array>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>CFBundleURLName</key>\n\t\t\t\t\t\t<string></string>\n\t\t\t\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t\t\t\t<array>\n\t\t\t\t\t\t\t<string>m.bilibili.com</string>\n\t\t\t\t\t\t\t<string>bilibili.com</string>\n\t\t\t\t\t\t\t<string>www.bilibili.com</string>\n\t\t\t\t\t\t\t<string>bangumi.bilibili.com</string>\n\t\t\t\t\t\t\t<string>bilibili.cn</string>\n\t\t\t\t\t\t\t<string>www.bilibili.cn</string>\n\t\t\t\t\t\t\t<string>bangumi.bilibili.cn</string>\n\t\t\t\t\t\t\t<string>bilibili.tv</string>\n\t\t\t\t\t\t\t<string>www.bilibili.tv</string>\n\t\t\t\t\t\t\t<string>bangumi.bilibili.tv</string>\n\t\t\t\t\t\t\t<string>miniapp.bilibili.com</string>\n\t\t\t\t\t\t\t<string>live.bilibili.com</string>\n\t\t\t\t\t\t</array>\n\t\t\t\t\t</dict>\n\t\t\t\t</array>\n\t\t\t</dict>\n\n\t\t\t<!-- 当其他应用程序或系统通过 bilibili -->\n\t\t\t<dict>\n\t\t\t\t<key>CFBundleURLName</key>\n\t\t\t\t<string>bilibili</string>\n\t\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>bilibili</string>\n\t\t\t\t</array>\n\t\t\t</dict>\n\t\t</array>\n\t\t<key>UIBackgroundModes</key>\n\t\t<array>\n\t\t\t<string>audio</string>\n\t\t</array>\n\t</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t2431C9E3151C7449D0D1C0F4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63F2789C7C1DF3CD2B80A936 /* Pods_Runner.framework */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t32E2926120A1A8DC0E629BC6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t3DA6FBBC55FDD1E3261D6D67 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.release.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t51DB54E5BB66F8608325A082 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.debug.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t63F2789C7C1DF3CD2B80A936 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t2431C9E3151C7449D0D1C0F4 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t31399C6148E878051E614578 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t63F2789C7C1DF3CD2B80A936 /* Pods_Runner.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\tEFBDDDF3F822865E6D1BB6D7 /* Pods */,\n\t\t\t\t31399C6148E878051E614578 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tEFBDDDF3F822865E6D1BB6D7 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t51DB54E5BB66F8608325A082 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t3DA6FBBC55FDD1E3261D6D67 /* Pods-Runner.release.xcconfig */,\n\t\t\t\t32E2926120A1A8DC0E629BC6 /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t67CCBB29D5A20A144E2BDF7D /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t\t5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */,\n\t\t\t\tB78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\",\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t67CCBB29D5A20A144E2BDF7D /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n\t\tB78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Copy Pods Resources\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.guozhigq.pilipala;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.guozhigq.pilipala;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.guozhigq.pilipala;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "lib/common/constants.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass StyleString {\n  static const double cardSpace = 8;\n  static const double safeSpace = 12;\n  static BorderRadius mdRadius = BorderRadius.circular(10);\n  static const Radius imgRadius = Radius.circular(10);\n  static const double aspectRatio = 16 / 10;\n}\n\nclass Constants {\n  // 27eb53fc9058f8c3  移动端 Android\n  // 4409e2ce8ffd12b8  TV端\n  static const String appKey = '4409e2ce8ffd12b8';\n  // 59b43e04ad6965f34319062b478f83dd TV端\n  static const String appSec = '59b43e04ad6965f34319062b478f83dd';\n  static const String thirdSign = '04224646d1fea004e79606d3b038c84a';\n  static const String thirdApi =\n      'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';\n}\n"
  },
  {
    "path": "lib/common/pages_bottom_sheet.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:scrollable_positioned_list/scrollable_positioned_list.dart';\nimport '../models/common/video_episode_type.dart';\n\nclass EpisodeBottomSheet {\n  final List<dynamic> episodes;\n  final int currentCid;\n  final dynamic dataType;\n  final BuildContext context;\n  final Function changeFucCall;\n  final int? cid;\n  final double? sheetHeight;\n  bool isFullScreen = false;\n\n  EpisodeBottomSheet({\n    required this.episodes,\n    required this.currentCid,\n    required this.dataType,\n    required this.context,\n    required this.changeFucCall,\n    this.cid,\n    this.sheetHeight,\n    this.isFullScreen = false,\n  });\n\n  Widget buildEpisodeListItem(\n    dynamic episode,\n    int index,\n    bool isCurrentIndex,\n  ) {\n    Color primary = Theme.of(context).colorScheme.primary;\n    Color onSurface = Theme.of(context).colorScheme.onSurface;\n\n    String title = '';\n    switch (dataType) {\n      case VideoEpidoesType.videoEpisode:\n        title = episode.title;\n        break;\n      case VideoEpidoesType.videoPart:\n        title = episode.pagePart;\n        break;\n      case VideoEpidoesType.bangumiEpisode:\n        title = '第${episode.title}话  ${episode.longTitle!}';\n        break;\n    }\n    return isFullScreen || episode?.cover == null || episode?.cover == ''\n        ? ListTile(\n            onTap: () {\n              SmartDialog.showToast('切换至「$title」');\n              changeFucCall.call(episode, index);\n            },\n            dense: false,\n            leading: isCurrentIndex\n                ? Image.asset(\n                    'assets/images/live.gif',\n                    color: primary,\n                    height: 12,\n                  )\n                : null,\n            title: Text(title,\n                style: TextStyle(\n                  fontSize: 14,\n                  color: isCurrentIndex ? primary : onSurface,\n                )))\n        : InkWell(\n            onTap: () {\n              SmartDialog.showToast('切换至「$title」');\n              changeFucCall.call(episode, index);\n            },\n            child: Padding(\n              padding:\n                  const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8),\n              child: Row(\n                children: [\n                  NetworkImgLayer(\n                      width: 130, height: 75, src: episode?.cover ?? ''),\n                  const SizedBox(width: 10),\n                  Expanded(\n                    child: Text(\n                      title,\n                      maxLines: 2,\n                      style: TextStyle(\n                        fontSize: 14,\n                        color: isCurrentIndex ? primary : onSurface,\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          );\n  }\n\n  Widget buildTitle() {\n    return AppBar(\n      toolbarHeight: 45,\n      automaticallyImplyLeading: false,\n      centerTitle: false,\n      title: Text(\n        '合集（${episodes.length}）',\n        style: Theme.of(context).textTheme.titleMedium,\n      ),\n      actions: !isFullScreen\n          ? [\n              IconButton(\n                icon: const Icon(Icons.close, size: 20),\n                onPressed: () => Navigator.pop(context),\n              ),\n              const SizedBox(width: 14),\n            ]\n          : null,\n    );\n  }\n\n  Widget buildShowContent(BuildContext context) {\n    final ItemScrollController itemScrollController = ItemScrollController();\n    int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid);\n    return StatefulBuilder(\n        builder: (BuildContext context, StateSetter setState) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        itemScrollController.jumpTo(index: currentIndex);\n      });\n      return Container(\n        height: sheetHeight,\n        color: Theme.of(context).colorScheme.surface,\n        child: Column(\n          children: [\n            buildTitle(),\n            Expanded(\n              child: Material(\n                child: PageStorage(\n                  bucket: PageStorageBucket(),\n                  child: ScrollablePositionedList.builder(\n                    itemScrollController: itemScrollController,\n                    itemCount: episodes.length + 1,\n                    itemBuilder: (BuildContext context, int index) {\n                      bool isLastItem = index == episodes.length;\n                      bool isCurrentIndex = currentIndex == index;\n                      return isLastItem\n                          ? SizedBox(\n                              height:\n                                  MediaQuery.of(context).padding.bottom + 20,\n                            )\n                          : buildEpisodeListItem(\n                              episodes[index],\n                              index,\n                              isCurrentIndex,\n                            );\n                    },\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ),\n      );\n    });\n  }\n\n  /// The [BuildContext] of the widget that calls the bottom sheet.\n  PersistentBottomSheetController show(BuildContext context) {\n    final PersistentBottomSheetController btmSheetCtr = showBottomSheet(\n      context: context,\n      builder: (BuildContext context) {\n        return buildShowContent(context);\n      },\n    );\n    return btmSheetCtr;\n  }\n}\n"
  },
  {
    "path": "lib/common/skeleton/dynamic_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'skeleton.dart';\n\nclass DynamicCardSkeleton extends StatelessWidget {\n  const DynamicCardSkeleton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Skeleton(\n      child: Container(\n        padding: const EdgeInsets.only(left: 12, right: 12, top: 12),\n        decoration: BoxDecoration(\n          border: Border(\n            bottom: BorderSide(\n              width: 8,\n              color: Theme.of(context).dividerColor.withOpacity(0.05),\n            ),\n          ),\n        ),\n        child: Column(\n          children: [\n            Row(\n              children: [\n                Container(\n                  width: 40,\n                  height: 40,\n                  decoration: BoxDecoration(\n                    color: Theme.of(context).colorScheme.onInverseSurface,\n                    borderRadius: BorderRadius.circular(20),\n                  ),\n                ),\n                const SizedBox(width: 10),\n                Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Container(\n                      color: Theme.of(context).colorScheme.onInverseSurface,\n                      width: 100,\n                      height: 13,\n                      margin: const EdgeInsets.only(bottom: 5),\n                    ),\n                    Container(\n                      color: Theme.of(context).colorScheme.onInverseSurface,\n                      width: 50,\n                      height: 11,\n                    ),\n                  ],\n                )\n              ],\n            ),\n            Container(\n              width: double.infinity,\n              margin: const EdgeInsets.only(top: 10),\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Container(\n                    color: Theme.of(context).colorScheme.onInverseSurface,\n                    width: double.infinity,\n                    height: 13,\n                    margin: const EdgeInsets.only(bottom: 7),\n                  ),\n                  Container(\n                    color: Theme.of(context).colorScheme.onInverseSurface,\n                    width: double.infinity,\n                    height: 13,\n                    margin: const EdgeInsets.only(bottom: 7),\n                  ),\n                  Container(\n                    color: Theme.of(context).colorScheme.onInverseSurface,\n                    width: 300,\n                    height: 13,\n                    margin: const EdgeInsets.only(bottom: 7),\n                  ),\n                  Container(\n                    color: Theme.of(context).colorScheme.onInverseSurface,\n                    width: 250,\n                    height: 13,\n                    margin: const EdgeInsets.only(bottom: 7),\n                  ),\n                  Container(\n                    color: Theme.of(context).colorScheme.onInverseSurface,\n                    width: 100,\n                    height: 13,\n                    margin: const EdgeInsets.only(bottom: 7),\n                  ),\n                ],\n              ),\n            ),\n            Row(\n              mainAxisAlignment: MainAxisAlignment.spaceAround,\n              children: [\n                for (var i = 0; i < 3; i++)\n                  TextButton.icon(\n                    onPressed: () {},\n                    icon: const Icon(\n                      Icons.radio_button_unchecked_outlined,\n                      size: 20,\n                    ),\n                    style: TextButton.styleFrom(\n                      padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),\n                      foregroundColor: Theme.of(context)\n                          .colorScheme\n                          .outline\n                          .withOpacity(0.2),\n                    ),\n                    label: Text(\n                      i == 0\n                          ? '转发'\n                          : i == 1\n                              ? '评论'\n                              : '点赞',\n                    ),\n                  )\n              ],\n            )\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/skeleton/media_bangumi.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/common/constants.dart';\n\nimport 'skeleton.dart';\n\nclass MediaBangumiSkeleton extends StatefulWidget {\n  const MediaBangumiSkeleton({super.key});\n\n  @override\n  State<MediaBangumiSkeleton> createState() => _MediaBangumiSkeletonState();\n}\n\nclass _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {\n  @override\n  Widget build(BuildContext context) {\n    Color bgColor = Theme.of(context).colorScheme.onInverseSurface;\n    return Skeleton(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(\n            StyleString.safeSpace, 7, StyleString.safeSpace, 7),\n        child: Row(\n          children: [\n            Container(\n              width: 111,\n              height: 148,\n              decoration: BoxDecoration(\n                  borderRadius: const BorderRadius.all(Radius.circular(6)),\n                  color: bgColor),\n            ),\n            const SizedBox(width: 10),\n            Expanded(\n              child: SizedBox(\n                height: 148,\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Container(\n                      color: Theme.of(context).colorScheme.onInverseSurface,\n                      width: 200,\n                      height: 20,\n                      margin: const EdgeInsets.only(bottom: 15),\n                    ),\n                    Container(\n                      color: Theme.of(context).colorScheme.onInverseSurface,\n                      width: 150,\n                      height: 13,\n                      margin: const EdgeInsets.only(bottom: 5),\n                    ),\n                    Container(\n                      color: Theme.of(context).colorScheme.onInverseSurface,\n                      width: 150,\n                      height: 13,\n                      margin: const EdgeInsets.only(bottom: 5),\n                    ),\n                    Container(\n                      color: Theme.of(context).colorScheme.onInverseSurface,\n                      width: 150,\n                      height: 13,\n                    ),\n                    const Spacer(),\n                    Container(\n                      width: 90,\n                      height: 35,\n                      decoration: BoxDecoration(\n                        borderRadius:\n                            const BorderRadius.all(Radius.circular(20)),\n                        color: Theme.of(context).colorScheme.onInverseSurface,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/skeleton/skeleton.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass Skeleton extends StatelessWidget {\n  final Widget child;\n\n  const Skeleton({\n    required this.child,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    var shimmerGradient = LinearGradient(\n      colors: [\n        Colors.transparent,\n        Theme.of(context).colorScheme.surface.withAlpha(10),\n        Theme.of(context).colorScheme.surface.withAlpha(10),\n        Colors.transparent,\n      ],\n      stops: const [\n        0.1,\n        0.3,\n        0.5,\n        0.7,\n      ],\n      begin: const Alignment(-1.0, -0.3),\n      end: const Alignment(1.0, 0.9),\n      tileMode: TileMode.clamp,\n    );\n    return Shimmer(\n      linearGradient: shimmerGradient,\n      child: ShimmerLoading(\n        isLoading: true,\n        child: child,\n      ),\n    );\n  }\n}\n\nclass Shimmer extends StatefulWidget {\n  static ShimmerState? of(BuildContext context) {\n    return context.findAncestorStateOfType<ShimmerState>();\n  }\n\n  const Shimmer({\n    super.key,\n    required this.linearGradient,\n    this.child,\n  });\n\n  final LinearGradient linearGradient;\n  final Widget? child;\n\n  @override\n  ShimmerState createState() => ShimmerState();\n}\n\nclass ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {\n  late AnimationController _shimmerController;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _shimmerController = AnimationController.unbounded(vsync: this)\n      ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));\n  }\n\n  @override\n  void dispose() {\n    _shimmerController.dispose();\n    super.dispose();\n  }\n\n  LinearGradient get gradient => LinearGradient(\n        colors: widget.linearGradient.colors,\n        stops: widget.linearGradient.stops,\n        begin: widget.linearGradient.begin,\n        end: widget.linearGradient.end,\n        transform: _SlidingGradientTransform(\n          slidePercent: _shimmerController.value,\n        ),\n      );\n\n  bool get isSized =>\n      (context.findRenderObject() as RenderBox?)?.hasSize ?? false;\n\n  Size get size => (context.findRenderObject() as RenderBox).size;\n\n  Offset getDescendantOffset({\n    required RenderBox descendant,\n    Offset offset = Offset.zero,\n  }) {\n    final shimmerBox = context.findRenderObject() as RenderBox;\n    return descendant.localToGlobal(offset, ancestor: shimmerBox);\n  }\n\n  Listenable get shimmerChanges => _shimmerController;\n\n  @override\n  Widget build(BuildContext context) {\n    return widget.child ?? const SizedBox();\n  }\n}\n\nclass _SlidingGradientTransform extends GradientTransform {\n  const _SlidingGradientTransform({\n    required this.slidePercent,\n  });\n\n  final double slidePercent;\n\n  @override\n  Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {\n    return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);\n  }\n}\n\nclass ShimmerLoading extends StatefulWidget {\n  const ShimmerLoading({\n    super.key,\n    required this.isLoading,\n    required this.child,\n  });\n\n  final bool isLoading;\n  final Widget child;\n\n  @override\n  State<ShimmerLoading> createState() => _ShimmerLoadingState();\n}\n\nclass _ShimmerLoadingState extends State<ShimmerLoading> {\n  Listenable? _shimmerChanges;\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    if (_shimmerChanges != null) {\n      _shimmerChanges!.removeListener(_onShimmerChange);\n    }\n    _shimmerChanges = Shimmer.of(context)?.shimmerChanges;\n    if (_shimmerChanges != null) {\n      _shimmerChanges!.addListener(_onShimmerChange);\n    }\n  }\n\n  @override\n  void dispose() {\n    _shimmerChanges?.removeListener(_onShimmerChange);\n    super.dispose();\n  }\n\n  void _onShimmerChange() {\n    if (widget.isLoading) {\n      setState(() {});\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (!widget.isLoading) {\n      return widget.child;\n    }\n\n    final shimmer = Shimmer.of(context)!;\n    if (!shimmer.isSized) {\n      return const SizedBox();\n    }\n    final shimmerSize = shimmer.size;\n    final gradient = shimmer.gradient;\n    final offsetWithinShimmer = shimmer.getDescendantOffset(\n      descendant: context.findRenderObject() as RenderBox,\n    );\n\n    return ShaderMask(\n      blendMode: BlendMode.srcATop,\n      shaderCallback: (bounds) {\n        return gradient.createShader(\n          Rect.fromLTWH(\n            -offsetWithinShimmer.dx,\n            -offsetWithinShimmer.dy,\n            shimmerSize.width,\n            shimmerSize.height,\n          ),\n        );\n      },\n      child: widget.child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/skeleton/video_card_h.dart",
    "content": "import 'package:pilipala/common/constants.dart';\nimport 'package:flutter/material.dart';\nimport 'skeleton.dart';\n\nclass VideoCardHSkeleton extends StatelessWidget {\n  const VideoCardHSkeleton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Skeleton(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(\n            StyleString.safeSpace, 7, StyleString.safeSpace, 7),\n        child: LayoutBuilder(\n          builder: (context, boxConstraints) {\n            double width =\n                (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;\n            return SizedBox(\n              height: width / StyleString.aspectRatio,\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.start,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  AspectRatio(\n                    aspectRatio: StyleString.aspectRatio,\n                    child: LayoutBuilder(\n                      builder: (context, boxConstraints) {\n                        return Container(\n                          decoration: BoxDecoration(\n                            color:\n                                Theme.of(context).colorScheme.onInverseSurface,\n                            borderRadius:\n                                BorderRadius.circular(StyleString.imgRadius.x),\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                  // VideoContent(videoItem: videoItem)\n                  Expanded(\n                      child: Padding(\n                    padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        Container(\n                          color: Theme.of(context).colorScheme.onInverseSurface,\n                          width: 200,\n                          height: 11,\n                          margin: const EdgeInsets.only(bottom: 5),\n                        ),\n                        Container(\n                          color: Theme.of(context).colorScheme.onInverseSurface,\n                          width: 150,\n                          height: 13,\n                        ),\n                        const Spacer(),\n                        Container(\n                          color: Theme.of(context).colorScheme.onInverseSurface,\n                          width: 100,\n                          height: 13,\n                          margin: const EdgeInsets.only(bottom: 5),\n                        ),\n                        Row(\n                          children: [\n                            Container(\n                              color: Theme.of(context)\n                                  .colorScheme\n                                  .onInverseSurface,\n                              width: 40,\n                              height: 13,\n                              margin: const EdgeInsets.only(right: 8),\n                            ),\n                            Container(\n                              color: Theme.of(context)\n                                  .colorScheme\n                                  .onInverseSurface,\n                              width: 40,\n                              height: 13,\n                            ),\n                          ],\n                        )\n                      ],\n                    ),\n                  )),\n                ],\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/skeleton/video_card_v.dart",
    "content": "import 'package:pilipala/common/constants.dart';\nimport 'package:flutter/material.dart';\nimport 'skeleton.dart';\n\nclass VideoCardVSkeleton extends StatelessWidget {\n  const VideoCardVSkeleton({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Skeleton(\n      child: Column(\n        children: [\n          AspectRatio(\n            aspectRatio: StyleString.aspectRatio,\n            child: LayoutBuilder(\n              builder: (context, boxConstraints) {\n                return Container(\n                  decoration: BoxDecoration(\n                      color: Theme.of(context).colorScheme.onInverseSurface,\n                      borderRadius:\n                          BorderRadius.circular(StyleString.imgRadius.x)),\n                );\n              },\n            ),\n          ),\n          Padding(\n            // 多列\n            padding: const EdgeInsets.fromLTRB(4, 5, 6, 6),\n            // 单列\n            // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                // const SizedBox(height: 6),\n                Container(\n                  width: 200,\n                  height: 13,\n                  margin: const EdgeInsets.only(bottom: 5),\n                  color: Theme.of(context).colorScheme.onInverseSurface,\n                ),\n                Container(\n                  width: 150,\n                  height: 13,\n                  margin: const EdgeInsets.only(bottom: 12),\n                  color: Theme.of(context).colorScheme.onInverseSurface,\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/skeleton/video_reply.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'skeleton.dart';\n\nclass VideoReplySkeleton extends StatelessWidget {\n  const VideoReplySkeleton({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    Color bgColor = Theme.of(context).colorScheme.onInverseSurface;\n    return Skeleton(\n      child: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.fromLTRB(12, 8, 8, 2),\n            child: Row(\n              children: [\n                ClipOval(\n                  child: Container(\n                    width: 34,\n                    height: 34,\n                    color: bgColor,\n                  ),\n                ),\n                const SizedBox(width: 12),\n                Container(\n                  width: 80,\n                  height: 13,\n                  color: bgColor,\n                )\n              ],\n            ),\n          ),\n          Container(\n            width: double.infinity,\n            margin:\n                const EdgeInsets.only(top: 4, left: 57, right: 6, bottom: 6),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              mainAxisAlignment: MainAxisAlignment.start,\n              children: [\n                Container(\n                  width: 300,\n                  height: 14,\n                  margin: const EdgeInsets.only(bottom: 4),\n                  color: bgColor,\n                ),\n                Container(\n                  width: 180,\n                  height: 14,\n                  margin: const EdgeInsets.only(bottom: 10),\n                  color: bgColor,\n                ),\n                Row(\n                  children: [\n                    Container(\n                      width: 40,\n                      height: 14,\n                      margin: const EdgeInsets.only(bottom: 4),\n                      color: bgColor,\n                    ),\n                    const Spacer(),\n                    Container(\n                      width: 30,\n                      height: 14,\n                      margin: const EdgeInsets.only(bottom: 4),\n                      color: bgColor,\n                    ),\n                    const SizedBox(width: 8),\n                    Container(\n                      width: 30,\n                      height: 14,\n                      margin: const EdgeInsets.only(bottom: 4),\n                      color: bgColor,\n                    ),\n                    const SizedBox(width: 8)\n                  ],\n                )\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/animated_dialog.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AnimatedDialog extends StatefulWidget {\n  const AnimatedDialog({Key? key, required this.child, this.closeFn})\n      : super(key: key);\n\n  final Widget child;\n  final Function? closeFn;\n\n  @override\n  State<StatefulWidget> createState() => AnimatedDialogState();\n}\n\nclass AnimatedDialogState extends State<AnimatedDialog>\n    with SingleTickerProviderStateMixin {\n  late AnimationController? controller;\n  late Animation<double>? opacityAnimation;\n  late Animation<double>? scaleAnimation;\n\n  @override\n  void initState() {\n    super.initState();\n\n    controller = AnimationController(\n        vsync: this, duration: const Duration(milliseconds: 800));\n    opacityAnimation = Tween<double>(begin: 0.0, end: 0.6).animate(\n        CurvedAnimation(parent: controller!, curve: Curves.easeOutExpo));\n    scaleAnimation =\n        CurvedAnimation(parent: controller!, curve: Curves.easeOutExpo);\n    controller!.addListener(() => setState(() {}));\n    controller!.forward();\n  }\n\n  @override\n  void dispose() {\n    controller!.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: Colors.black.withOpacity(opacityAnimation!.value),\n      child: InkWell(\n        splashColor: Colors.transparent,\n        onTap: () => widget.closeFn!(),\n        child: Center(\n          child: FadeTransition(\n            opacity: scaleAnimation!,\n            child: ScaleTransition(\n              scale: scaleAnimation!,\n              child: widget.child,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/app_expansion_panel_list.dart",
    "content": "import 'package:flutter/material.dart';\n\nconst double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;\n\nclass _SaltedKey<S, V> extends LocalKey {\n  const _SaltedKey(this.salt, this.value);\n\n  final S salt;\n  final V value;\n\n  @override\n  bool operator ==(Object other) {\n    if (other.runtimeType != runtimeType) return false;\n    return other is _SaltedKey<S, V> &&\n        other.salt == salt &&\n        other.value == value;\n  }\n\n  @override\n  int get hashCode => Object.hash(runtimeType, salt, value);\n\n  @override\n  String toString() {\n    final String saltString = S == String ? \"<'$salt'>\" : '<$salt>';\n    final String valueString = V == String ? \"<'$value'>\" : '<$value>';\n    return '[$saltString $valueString]';\n  }\n}\n\nclass AppExpansionPanelList extends StatefulWidget {\n  /// Creates an expansion panel list widget. The [expansionCallback] is\n  /// triggered when an expansion panel expand/collapse button is pushed.\n  ///\n  /// The [children] and [animationDuration] arguments must not be null.\n  const AppExpansionPanelList({\n    super.key,\n    required this.children,\n    this.expansionCallback,\n    this.animationDuration = kThemeAnimationDuration,\n    this.expandedHeaderPadding = EdgeInsets.zero,\n    this.dividerColor,\n    this.elevation = 2,\n  })  : _allowOnlyOnePanelOpen = false,\n        initialOpenPanelValue = null;\n\n  /// The children of the expansion panel list. They are laid out in a similar\n  /// fashion to [ListBody].\n  final List<AppExpansionPanel> children;\n\n  /// The callback that gets called whenever one of the expand/collapse buttons\n  /// is pressed. The arguments passed to the callback are the index of the\n  /// pressed panel and whether the panel is currently expanded or not.\n  ///\n  /// If AppExpansionPanelList.radio is used, the callback may be called a\n  /// second time if a different panel was previously open. The arguments\n  /// passed to the second callback are the index of the panel that will close\n  /// and false, marking that it will be closed.\n  ///\n  /// For AppExpansionPanelList, the callback needs to setState when it's notified\n  /// about the closing/opening panel. On the other hand, the callback for\n  /// AppExpansionPanelList.radio is simply meant to inform the parent widget of\n  /// changes, as the radio panels' open/close states are managed internally.\n  ///\n  /// This callback is useful in order to keep track of the expanded/collapsed\n  /// panels in a parent widget that may need to react to these changes.\n  final ExpansionPanelCallback? expansionCallback;\n\n  /// The duration of the expansion animation.\n  final Duration animationDuration;\n\n  // Whether multiple panels can be open simultaneously\n  final bool _allowOnlyOnePanelOpen;\n\n  /// The value of the panel that initially begins open. (This value is\n  /// only used when initializing with the [AppExpansionPanelList.radio]\n  /// constructor.)\n  final Object? initialOpenPanelValue;\n\n  /// The padding that surrounds the panel header when expanded.\n  ///\n  /// By default, 16px of space is added to the header vertically (above and below)\n  /// during expansion.\n  final EdgeInsets expandedHeaderPadding;\n\n  /// Defines color for the divider when [AppExpansionPanel.isExpanded] is false.\n  ///\n  /// If `dividerColor` is null, then [DividerThemeData.color] is used. If that\n  /// is null, then [ThemeData.dividerColor] is used.\n  final Color? dividerColor;\n\n  /// Defines elevation for the [AppExpansionPanel] while it's expanded.\n  ///\n  /// By default, the value of elevation is 2.\n  final double elevation;\n\n  @override\n  State<AppExpansionPanelList> createState() => _AppExpansionPanelListState();\n}\n\nclass _AppExpansionPanelListState extends State<AppExpansionPanelList> {\n  ExpansionPanelRadio? _currentOpenPanel;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget._allowOnlyOnePanelOpen) {\n      assert(_allIdentifiersUnique(),\n          'All ExpansionPanelRadio identifier values must be unique.');\n      if (widget.initialOpenPanelValue != null) {\n        _currentOpenPanel = searchPanelByValue(\n            widget.children.cast<ExpansionPanelRadio>(),\n            widget.initialOpenPanelValue);\n      }\n    }\n  }\n\n  @override\n  void didUpdateWidget(AppExpansionPanelList oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (widget._allowOnlyOnePanelOpen) {\n      assert(_allIdentifiersUnique(),\n          'All ExpansionPanelRadio identifier values must be unique.');\n      // If the previous widget was non-radio AppExpansionPanelList, initialize the\n      // open panel to widget.initialOpenPanelValue\n      if (!oldWidget._allowOnlyOnePanelOpen) {\n        _currentOpenPanel = searchPanelByValue(\n            widget.children.cast<ExpansionPanelRadio>(),\n            widget.initialOpenPanelValue);\n      }\n    } else {\n      _currentOpenPanel = null;\n    }\n  }\n\n  bool _allIdentifiersUnique() {\n    final Map<Object, bool> identifierMap = <Object, bool>{};\n    for (final ExpansionPanelRadio child\n        in widget.children.cast<ExpansionPanelRadio>()) {\n      identifierMap[child.value] = true;\n    }\n    return identifierMap.length == widget.children.length;\n  }\n\n  bool _isChildExpanded(int index) {\n    if (widget._allowOnlyOnePanelOpen) {\n      final ExpansionPanelRadio radioWidget =\n          widget.children[index] as ExpansionPanelRadio;\n      return _currentOpenPanel?.value == radioWidget.value;\n    }\n    return widget.children[index].isExpanded;\n  }\n\n  void _handlePressed(bool isExpanded, int index) {\n    widget.expansionCallback?.call(index, isExpanded);\n\n    if (widget._allowOnlyOnePanelOpen) {\n      final ExpansionPanelRadio pressedChild =\n          widget.children[index] as ExpansionPanelRadio;\n\n      // If another ExpansionPanelRadio was already open, apply its\n      // expansionCallback (if any) to false, because it's closing.\n      for (int childIndex = 0;\n          childIndex < widget.children.length;\n          childIndex += 1) {\n        final ExpansionPanelRadio child =\n            widget.children[childIndex] as ExpansionPanelRadio;\n        if (widget.expansionCallback != null &&\n            childIndex != index &&\n            child.value == _currentOpenPanel?.value) {\n          widget.expansionCallback!(childIndex, false);\n        }\n      }\n\n      setState(() {\n        _currentOpenPanel = isExpanded ? null : pressedChild;\n      });\n    }\n  }\n\n  ExpansionPanelRadio? searchPanelByValue(\n      List<ExpansionPanelRadio> panels, Object? value) {\n    for (final ExpansionPanelRadio panel in panels) {\n      if (panel.value == value) return panel;\n    }\n    return null;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    assert(\n      kElevationToShadow.containsKey(widget.elevation),\n      'Invalid value for elevation. See the kElevationToShadow constant for'\n      ' possible elevation values.',\n    );\n\n    final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];\n\n    for (int index = 0; index < widget.children.length; index += 1) {\n      //todo: Uncomment to add gap between selected panels\n      /*if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))\n        items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));*/\n\n      final AppExpansionPanel child = widget.children[index];\n      final Widget headerWidget = child.headerBuilder(\n        context,\n        _isChildExpanded(index),\n      );\n\n      Widget? expandIconContainer = ExpandIcon(\n        isExpanded: _isChildExpanded(index),\n        onPressed: !child.canTapOnHeader\n            ? (bool isExpanded) => _handlePressed(isExpanded, index)\n            : null,\n      );\n      if (!child.canTapOnHeader) {\n        final MaterialLocalizations localizations =\n            MaterialLocalizations.of(context);\n        expandIconContainer = Semantics(\n          label: _isChildExpanded(index)\n              ? localizations.expandedIconTapHint\n              : localizations.collapsedIconTapHint,\n          container: true,\n          child: expandIconContainer,\n        );\n      }\n\n      final iconContainer = child.iconBuilder;\n      if (iconContainer != null) {\n        expandIconContainer = iconContainer(\n          expandIconContainer,\n          _isChildExpanded(index),\n        );\n      }\n\n      Widget header = Row(\n        children: <Widget>[\n          Expanded(\n            child: AnimatedContainer(\n              duration: widget.animationDuration,\n              curve: Curves.fastOutSlowIn,\n              margin: _isChildExpanded(index)\n                  ? widget.expandedHeaderPadding\n                  : EdgeInsets.zero,\n              child: ConstrainedBox(\n                constraints: const BoxConstraints(\n                    minHeight: _kPanelHeaderCollapsedHeight),\n                child: headerWidget,\n              ),\n            ),\n          ),\n          if (expandIconContainer != null) expandIconContainer,\n        ],\n      );\n      if (child.canTapOnHeader) {\n        header = MergeSemantics(\n          child: InkWell(\n            onTap: () => _handlePressed(_isChildExpanded(index), index),\n            child: header,\n          ),\n        );\n      }\n      items.add(\n        MaterialSlice(\n          key: _SaltedKey<BuildContext, int>(context, index * 2),\n          color: child.backgroundColor,\n          child: Column(\n            children: <Widget>[\n              header,\n              AnimatedCrossFade(\n                firstChild: Container(height: 0.0),\n                secondChild: child.body,\n                firstCurve:\n                    const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),\n                secondCurve:\n                    const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),\n                sizeCurve: Curves.fastOutSlowIn,\n                crossFadeState: _isChildExpanded(index)\n                    ? CrossFadeState.showSecond\n                    : CrossFadeState.showFirst,\n                duration: widget.animationDuration,\n              ),\n            ],\n          ),\n        ),\n      );\n\n      if (_isChildExpanded(index) && index != widget.children.length - 1) {\n        items.add(MaterialGap(\n            key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));\n      }\n    }\n\n    return MergeableMaterial(\n      hasDividers: true,\n      dividerColor: widget.dividerColor,\n      elevation: widget.elevation,\n      children: items,\n    );\n  }\n}\n\ntypedef ExpansionPanelIconBuilder = Widget? Function(\n  Widget child,\n  bool isExpanded,\n);\n\nclass AppExpansionPanel {\n  /// Creates an expansion panel to be used as a child for [ExpansionPanelList].\n  /// See [ExpansionPanelList] for an example on how to use this widget.\n  ///\n  /// The [headerBuilder], [body], and [isExpanded] arguments must not be null.\n  AppExpansionPanel({\n    required this.headerBuilder,\n    required this.body,\n    this.iconBuilder,\n    this.isExpanded = false,\n    this.canTapOnHeader = false,\n    this.backgroundColor,\n  });\n\n  /// The widget builder that builds the expansion panels' header.\n  final ExpansionPanelHeaderBuilder headerBuilder;\n\n  /// The widget builder that builds the expansion panels' icon.\n  ///\n  /// If not pass any function, then default icon will be displayed.\n  ///\n  /// If builder function return null, then icon will not displayed.\n  final ExpansionPanelIconBuilder? iconBuilder;\n\n  /// The body of the expansion panel that's displayed below the header.\n  ///\n  /// This widget is visible only when the panel is expanded.\n  final Widget body;\n\n  /// Whether the panel is expanded.\n  ///\n  /// Defaults to false.\n  final bool isExpanded;\n\n  /// Whether tapping on the panel's header will expand/collapse it.\n  ///\n  /// Defaults to false.\n  final bool canTapOnHeader;\n\n  /// Defines the background color of the panel.\n  ///\n  /// Defaults to [ThemeData.cardColor].\n  final Color? backgroundColor;\n}\n"
  },
  {
    "path": "lib/common/widgets/appbar.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppBarWidget extends StatelessWidget implements PreferredSizeWidget {\n  const AppBarWidget({\n    required this.child,\n    required this.controller,\n    required this.visible,\n    Key? key,\n  }) : super(key: key);\n\n  final PreferredSizeWidget child;\n  final AnimationController controller;\n  final bool visible;\n\n  @override\n  Size get preferredSize => child.preferredSize;\n\n  @override\n  Widget build(BuildContext context) {\n    visible ? controller.reverse() : controller.forward();\n    return SlideTransition(\n      position: Tween<Offset>(\n        begin: Offset.zero,\n        end: const Offset(0, -1),\n      ).animate(CurvedAnimation(\n        parent: controller,\n        curve: Curves.easeInOutBack,\n      )),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/badge.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass PBadge extends StatelessWidget {\n  final String? text;\n  final double? top;\n  final double? right;\n  final double? bottom;\n  final double? left;\n  final String? type;\n  final String? size;\n  final String? stack;\n  final double? fs;\n\n  const PBadge({\n    super.key,\n    this.text,\n    this.top,\n    this.right,\n    this.bottom,\n    this.left,\n    this.type = 'primary',\n    this.size = 'medium',\n    this.stack = 'position',\n    this.fs = 11,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    ColorScheme t = Theme.of(context).colorScheme;\n    // 背景色\n    Color bgColor = t.primary;\n    // 前景色\n    Color color = t.onPrimary;\n    // 边框色\n    Color borderColor = Colors.transparent;\n    if (type == 'gray') {\n      bgColor = Colors.black54.withOpacity(0.4);\n      color = Colors.white;\n    }\n    if (type == 'color') {\n      bgColor = t.primaryContainer.withOpacity(0.6);\n      color = t.primary;\n    }\n    if (type == 'line') {\n      bgColor = Colors.transparent;\n      color = t.primary;\n      borderColor = t.primary;\n    }\n\n    EdgeInsets paddingStyle =\n        const EdgeInsets.symmetric(vertical: 1, horizontal: 6);\n    double fontSize = 11;\n    BorderRadius br = BorderRadius.circular(4);\n\n    if (size == 'small') {\n      paddingStyle = const EdgeInsets.symmetric(vertical: 0, horizontal: 3);\n      fontSize = 11;\n      br = BorderRadius.circular(3);\n    }\n\n    Widget content = Container(\n      padding: paddingStyle,\n      decoration: BoxDecoration(\n        borderRadius: br,\n        color: bgColor,\n        border: Border.all(color: borderColor),\n      ),\n      child: Text(\n        text ?? '',\n        style: TextStyle(fontSize: fs ?? fontSize, color: color),\n      ),\n    );\n    if (stack == 'position') {\n      return Positioned(\n        top: top,\n        left: left,\n        right: right,\n        bottom: bottom,\n        child: content,\n      );\n    } else {\n      return Padding(\n        padding: const EdgeInsets.only(right: 5),\n        child: content,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/content_container.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ContentContainer extends StatelessWidget {\n  final Widget? contentWidget;\n  final Widget? bottomWidget;\n  final bool isScrollable;\n  final Clip? childClipBehavior;\n\n  const ContentContainer(\n      {Key? key,\n      this.contentWidget,\n      this.bottomWidget,\n      this.isScrollable = true,\n      this.childClipBehavior})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        return SingleChildScrollView(\n          clipBehavior: childClipBehavior ?? Clip.hardEdge,\n          physics: isScrollable ? null : const NeverScrollableScrollPhysics(),\n          child: ConstrainedBox(\n            constraints: constraints.copyWith(\n              minHeight: constraints.maxHeight,\n              maxHeight: double.infinity,\n            ),\n            child: IntrinsicHeight(\n              child: Column(\n                children: <Widget>[\n                  if (contentWidget != null)\n                    Expanded(\n                      child: contentWidget!,\n                    )\n                  else\n                    const Spacer(),\n                  if (bottomWidget != null) bottomWidget!,\n                ],\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/custom_toast.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nBox<dynamic> setting = GStrorage.setting;\n\nclass CustomToast extends StatelessWidget {\n  const CustomToast({super.key, required this.msg});\n\n  final String msg;\n\n  @override\n  Widget build(BuildContext context) {\n    final double toastOpacity =\n        setting.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0) as double;\n    return Container(\n      margin:\n          EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 30),\n      padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),\n      decoration: BoxDecoration(\n        color: Theme.of(context)\n            .colorScheme\n            .primaryContainer\n            .withOpacity(toastOpacity),\n        borderRadius: BorderRadius.circular(20),\n      ),\n      child: Text(\n        msg,\n        style: TextStyle(\n          fontSize: 13,\n          color: Theme.of(context).colorScheme.primary,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/html_render.dart",
    "content": "import 'package:cached_network_image/cached_network_image.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_html/flutter_html.dart';\nimport 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';\nimport 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';\nimport 'package:pilipala/utils/highlight.dart';\n\n// ignore: must_be_immutable\nclass HtmlRender extends StatelessWidget {\n  const HtmlRender({\n    this.htmlContent,\n    this.imgCount,\n    this.imgList,\n    super.key,\n  });\n\n  final String? htmlContent;\n  final int? imgCount;\n  final List<String>? imgList;\n\n  @override\n  Widget build(BuildContext context) {\n    return Html(\n      data: htmlContent,\n      onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},\n      extensions: [\n        TagExtension(\n          tagsToExtend: <String>{'pre'},\n          builder: (ExtensionContext extensionContext) {\n            final Map<String, dynamic> attributes = extensionContext.attributes;\n            final String lang = attributes['data-lang'] as String;\n            final String code = attributes['codecontent'] as String;\n            List<String> selectedLanguages = [lang.split('@').first];\n            TextSpan? result = highlightExistingText(code, selectedLanguages);\n            if (result == null) {\n              return const Center(child: Text('代码块渲染失败'));\n            }\n            return SelectableText.rich(result);\n          },\n        ),\n        TagExtension(\n          tagsToExtend: <String>{'img'},\n          builder: (ExtensionContext extensionContext) {\n            try {\n              final Map<String, dynamic> attributes =\n                  extensionContext.attributes;\n              final List<dynamic> key = attributes.keys.toList();\n              String imgUrl = key.contains('src')\n                  ? attributes['src'] as String\n                  : attributes['data-src'] as String;\n              if (imgUrl.startsWith('//')) {\n                imgUrl = 'https:$imgUrl';\n              }\n              if (imgUrl.startsWith('http://')) {\n                imgUrl = imgUrl.replaceAll('http://', 'https://');\n              }\n              imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;\n              final bool isEmote = imgUrl.contains('/emote/');\n              final bool isMall = imgUrl.contains('/mall/');\n              if (isMall) {\n                return const SizedBox();\n              }\n              return InkWell(\n                onTap: () {\n                  Navigator.of(context).push(\n                    HeroDialogRoute<void>(\n                      builder: (BuildContext context) =>\n                          InteractiveviewerGallery(\n                        sources: imgList ?? [imgUrl],\n                        initIndex: imgList?.indexOf(imgUrl) ?? 0,\n                        itemBuilder: (\n                          BuildContext context,\n                          int index,\n                          bool isFocus,\n                          bool enablePageView,\n                        ) {\n                          return GestureDetector(\n                            behavior: HitTestBehavior.opaque,\n                            onTap: () {\n                              if (enablePageView) {\n                                Navigator.of(context).pop();\n                              }\n                            },\n                            child: Center(\n                              child: Hero(\n                                tag: imgList?[index] ?? imgUrl,\n                                child: CachedNetworkImage(\n                                  fadeInDuration:\n                                      const Duration(milliseconds: 0),\n                                  imageUrl: imgList?[index] ?? imgUrl,\n                                  fit: BoxFit.contain,\n                                ),\n                              ),\n                            ),\n                          );\n                        },\n                        onPageChanged: (int pageIndex) {},\n                      ),\n                    ),\n                  );\n                },\n                child: CachedNetworkImage(imageUrl: imgUrl),\n              );\n              // return NetworkImgLayer(\n              //   width: isEmote ? 22 : Get.size.width - 24,\n              //   height: isEmote ? 22 : 200,\n              //   src: imgUrl,\n              // );\n            } catch (err) {\n              return const SizedBox();\n            }\n          },\n        ),\n      ],\n      style: {\n        'html': Style(\n          fontSize: FontSize.large,\n          lineHeight: LineHeight.percent(140),\n        ),\n        'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),\n        'a': Style(\n          color: Theme.of(context).colorScheme.primary,\n          textDecoration: TextDecoration.none,\n        ),\n        'p': Style(\n          margin: Margins.only(bottom: 10),\n        ),\n        'span': Style(\n          fontSize: FontSize.large,\n          height: Height(1.65),\n        ),\n        'div': Style(height: Height.auto()),\n        'li > p': Style(\n          display: Display.inline,\n        ),\n        'li': Style(\n          padding: HtmlPaddings.only(bottom: 4),\n          textAlign: TextAlign.justify,\n        ),\n        'img': Style(margin: Margins.only(top: 4, bottom: 4)),\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/http_error.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\n\nclass HttpError extends StatelessWidget {\n  const HttpError({\n    required this.errMsg,\n    required this.fn,\n    this.btnText,\n    this.isShowBtn = true,\n    super.key,\n  });\n\n  final String? errMsg;\n  final Function()? fn;\n  final String? btnText;\n  final bool isShowBtn;\n\n  @override\n  Widget build(BuildContext context) {\n    return SliverToBoxAdapter(\n      child: SizedBox(\n        height: 400,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.center,\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            SvgPicture.asset(\n              \"assets/images/error.svg\",\n              height: 200,\n            ),\n            const SizedBox(height: 30),\n            Text(\n              errMsg ?? '请求异常',\n              textAlign: TextAlign.center,\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n            const SizedBox(height: 20),\n            if (isShowBtn)\n              FilledButton.tonal(\n                onPressed: () {\n                  fn!();\n                },\n                style: ButtonStyle(\n                  backgroundColor: MaterialStateProperty.resolveWith((states) {\n                    return Theme.of(context).colorScheme.primary.withAlpha(20);\n                  }),\n                ),\n                child: Text(\n                  btnText ?? '点击重试',\n                  style:\n                      TextStyle(color: Theme.of(context).colorScheme.primary),\n                ),\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/live_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport '../../utils/utils.dart';\nimport '../constants.dart';\nimport 'network_img_layer.dart';\n\nclass LiveCard extends StatelessWidget {\n  // ignore: prefer_typing_uninitialized_variables\n  final dynamic liveItem;\n\n  const LiveCard({\n    Key? key,\n    required this.liveItem,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    final String heroTag = Utils.makeHeroTag(liveItem.roomid);\n\n    return Card(\n      elevation: 0,\n      clipBehavior: Clip.hardEdge,\n      shape: RoundedRectangleBorder(\n        borderRadius: BorderRadius.circular(0),\n        side: BorderSide(\n          color: Theme.of(context).dividerColor.withOpacity(0.08),\n        ),\n      ),\n      margin: EdgeInsets.zero,\n      child: InkWell(\n        onTap: () {},\n        child: Column(\n          children: [\n            AspectRatio(\n              aspectRatio: StyleString.aspectRatio,\n              child: LayoutBuilder(builder:\n                  (BuildContext context, BoxConstraints boxConstraints) {\n                final double maxWidth = boxConstraints.maxWidth;\n                final double maxHeight = boxConstraints.maxHeight;\n                return Stack(\n                  children: [\n                    Hero(\n                      tag: heroTag,\n                      child: NetworkImgLayer(\n                        src: liveItem.cover as String,\n                        type: 'emote',\n                        width: maxWidth,\n                        height: maxHeight,\n                      ),\n                    ),\n                    Positioned(\n                      left: 0,\n                      right: 0,\n                      bottom: 0,\n                      child: AnimatedOpacity(\n                        opacity: 1,\n                        duration: const Duration(milliseconds: 200),\n                        child: LiveStat(\n                          // view: liveItem.stat.view,\n                          // danmaku: liveItem.stat.danmaku,\n                          // duration: liveItem.duration,\n                          online: liveItem.online as int,\n                        ),\n                      ),\n                    ),\n                  ],\n                );\n              }),\n            ),\n            LiveContent(liveItem: liveItem)\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass LiveContent extends StatelessWidget {\n  // ignore: prefer_typing_uninitialized_variables\n  final liveItem;\n  const LiveContent({Key? key, required this.liveItem}) : super(key: key);\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      // 多列\n      padding: const EdgeInsets.fromLTRB(8, 8, 6, 7),\n      // 单列\n      // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          Text(\n            liveItem.title as String,\n            textAlign: TextAlign.start,\n            style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),\n            maxLines: 2,\n            overflow: TextOverflow.ellipsis,\n          ),\n          SizedBox(\n            width: double.infinity,\n            child: Text(\n              liveItem.uname as String,\n              maxLines: 1,\n              style: TextStyle(\n                fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                color: Theme.of(context).colorScheme.outline,\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass LiveStat extends StatelessWidget {\n  const LiveStat({super.key, required this.online});\n\n  final int? online;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 45,\n      padding: const EdgeInsets.only(top: 22, left: 8, right: 8),\n      decoration: const BoxDecoration(\n        gradient: LinearGradient(\n          begin: Alignment.topCenter,\n          end: Alignment.bottomCenter,\n          colors: <Color>[\n            Colors.transparent,\n            Colors.black54,\n          ],\n          tileMode: TileMode.mirror,\n        ),\n      ),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: <Widget>[\n          // Row(\n          // children: [\n          // StatView(\n          //   theme: 'white',\n          //   view: view,\n          // ),\n          // const SizedBox(width: 8),\n          // StatDanMu(\n          //   theme: 'white',\n          //   danmu: danmaku,\n          // ),\n          // ],\n          // ),\n          Text(\n            online.toString(),\n            style: const TextStyle(fontSize: 11, color: Colors.white),\n          )\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/network_img_layer.dart",
    "content": "import 'package:cached_network_image/cached_network_image.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/utils/extension.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\nimport '../../utils/storage.dart';\nimport '../constants.dart';\n\nBox<dynamic> setting = GStrorage.setting;\n\nclass NetworkImgLayer extends StatelessWidget {\n  const NetworkImgLayer({\n    super.key,\n    this.src,\n    required this.width,\n    required this.height,\n    this.type,\n    this.fadeOutDuration,\n    this.fadeInDuration,\n    // 图片质量 默认1%\n    this.quality,\n    this.origAspectRatio,\n  });\n\n  final String? src;\n  final double width;\n  final double height;\n  final String? type;\n  final Duration? fadeOutDuration;\n  final Duration? fadeInDuration;\n  final int? quality;\n  final double? origAspectRatio;\n\n  @override\n  Widget build(BuildContext context) {\n    final int defaultImgQuality = GlobalDataCache().imgQuality;\n    if (src == '' || src == null) {\n      return placeholder(context);\n    }\n    final String imageUrl =\n        '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp';\n    int? memCacheWidth, memCacheHeight;\n    double aspectRatio = (width / height).toDouble();\n\n    void setMemCacheSizes() {\n      if (aspectRatio > 1) {\n        memCacheHeight = height.cacheSize(context);\n      } else if (aspectRatio < 1) {\n        memCacheWidth = width.cacheSize(context);\n      } else {\n        if (origAspectRatio != null && origAspectRatio! > 1) {\n          memCacheWidth = width.cacheSize(context);\n        } else if (origAspectRatio != null && origAspectRatio! < 1) {\n          memCacheHeight = height.cacheSize(context);\n        } else {\n          memCacheWidth = width.cacheSize(context);\n          memCacheHeight = height.cacheSize(context);\n        }\n      }\n    }\n\n    setMemCacheSizes();\n\n    if (memCacheWidth == null && memCacheHeight == null) {\n      memCacheWidth = width.toInt();\n    }\n\n    return src != '' && src != null\n        ? ClipRRect(\n            clipBehavior: Clip.antiAlias,\n            borderRadius: BorderRadius.circular(\n              type == 'avatar'\n                  ? 50\n                  : type == 'emote'\n                      ? 0\n                      : StyleString.imgRadius.x,\n            ),\n            child: CachedNetworkImage(\n              imageUrl: imageUrl,\n              width: width,\n              height: height,\n              memCacheWidth: memCacheWidth,\n              memCacheHeight: memCacheHeight,\n              fit: BoxFit.cover,\n              fadeOutDuration:\n                  fadeOutDuration ?? const Duration(milliseconds: 120),\n              fadeInDuration:\n                  fadeInDuration ?? const Duration(milliseconds: 120),\n              filterQuality: FilterQuality.low,\n              errorWidget: (BuildContext context, String url, Object error) =>\n                  placeholder(context),\n              placeholder: (BuildContext context, String url) =>\n                  placeholder(context),\n            ),\n          )\n        : placeholder(context);\n  }\n\n  Widget placeholder(BuildContext context) {\n    return Container(\n      width: width,\n      height: height,\n      clipBehavior: Clip.antiAlias,\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),\n        borderRadius: BorderRadius.circular(type == 'avatar'\n            ? 50\n            : type == 'emote'\n                ? 0\n                : StyleString.imgRadius.x),\n      ),\n      child: type == 'bg'\n          ? const SizedBox()\n          : Center(\n              child: Image.asset(\n                type == 'avatar'\n                    ? 'assets/images/noface.jpeg'\n                    : 'assets/images/loading.png',\n                width: width,\n                height: height,\n                cacheWidth: width.cacheSize(context),\n                cacheHeight: height.cacheSize(context),\n              ),\n            ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/no_data.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\n\nclass NoData extends StatelessWidget {\n  const NoData({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SliverToBoxAdapter(\n      child: SizedBox(\n        height: 400,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.center,\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            SvgPicture.asset(\n              \"assets/images/error.svg\",\n              height: 200,\n            ),\n            const SizedBox(height: 20),\n            Text(\n              '没有数据',\n              textAlign: TextAlign.center,\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/sliver_header.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass SliverHeaderDelegate extends SliverPersistentHeaderDelegate {\n  SliverHeaderDelegate({required this.height, required this.child});\n\n  final double height;\n  final Widget child;\n\n  @override\n  Widget build(\n      BuildContext context, double shrinkOffset, bool overlapsContent) {\n    return child;\n  }\n\n  @override\n  double get maxExtent => height;\n\n  @override\n  double get minExtent => height;\n\n  @override\n  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>\n      true;\n}\n"
  },
  {
    "path": "lib/common/widgets/stat/danmu.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass StatDanMu extends StatelessWidget {\n  final String? theme;\n  final dynamic danmu;\n  final String? size;\n\n  const StatDanMu({Key? key, this.theme = 'gray', this.danmu, this.size})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    Map<String, Color> colorObject = {\n      'white': Colors.white,\n      'gray': Theme.of(context).colorScheme.outline,\n      'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),\n    };\n    Color color = colorObject[theme]!;\n    return StatIconText(\n      icon: Icons.subtitles_outlined,\n      text: Utils.numFormat(danmu!),\n      color: color,\n      size: size,\n    );\n  }\n}\n\nclass StatIconText extends StatelessWidget {\n  final IconData icon;\n  final String text;\n  final Color color;\n  final String? size;\n\n  const StatIconText({\n    Key? key,\n    required this.icon,\n    required this.text,\n    required this.color,\n    this.size,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Icon(\n          icon,\n          size: 14,\n          color: color,\n        ),\n        const SizedBox(width: 2),\n        Text(\n          text,\n          style: TextStyle(\n            fontSize: size == 'medium' ? 12 : 11,\n            color: color,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/stat/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass StatView extends StatelessWidget {\n  final String? theme;\n  final dynamic view;\n  final String? size;\n\n  const StatView({\n    Key? key,\n    this.theme = 'gray',\n    this.view,\n    this.size,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    Map<String, Color> colorObject = {\n      'white': Colors.white,\n      'gray': Theme.of(context).colorScheme.outline,\n      'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),\n    };\n    Color color = colorObject[theme]!;\n    return StatIconText(\n      icon: Icons.play_circle_outlined,\n      text: Utils.numFormat(view!),\n      color: color,\n      size: size,\n    );\n  }\n}\n\nclass StatIconText extends StatelessWidget {\n  final IconData icon;\n  final String text;\n  final Color color;\n  final String? size;\n\n  const StatIconText({\n    Key? key,\n    required this.icon,\n    required this.text,\n    required this.color,\n    this.size,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Icon(\n          icon,\n          size: 13,\n          color: color,\n        ),\n        const SizedBox(width: 2),\n        Text(\n          text,\n          style: TextStyle(\n            fontSize: size == 'medium' ? 12 : 11,\n            color: color,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/video_card_h.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/constants.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/image_save.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport 'package:pilipala/utils/url_utils.dart';\nimport '../../http/search.dart';\nimport '../../http/user.dart';\nimport '../../http/video.dart';\nimport '../../utils/utils.dart';\nimport '../constants.dart';\nimport 'badge.dart';\nimport 'network_img_layer.dart';\nimport 'stat/danmu.dart';\nimport 'stat/view.dart';\n\n// 视频卡片 - 水平布局\nclass VideoCardH extends StatelessWidget {\n  const VideoCardH({\n    super.key,\n    required this.videoItem,\n    this.onPressedFn,\n    this.source = 'normal',\n    this.showOwner = true,\n    this.showView = true,\n    this.showDanmaku = true,\n    this.showPubdate = false,\n    this.showCharge = false,\n  });\n  // ignore: prefer_typing_uninitialized_variables\n  final videoItem;\n  final Function()? onPressedFn;\n  // normal 推荐, later 稍后再看, search 搜索\n  final String source;\n  final bool showOwner;\n  final bool showView;\n  final bool showDanmaku;\n  final bool showPubdate;\n  final bool showCharge;\n\n  @override\n  Widget build(BuildContext context) {\n    final int aid = videoItem.aid;\n    final String bvid = videoItem.bvid;\n    String type = 'video';\n    try {\n      type = videoItem.type;\n    } catch (_) {}\n    final String heroTag = Utils.makeHeroTag(aid);\n    return InkWell(\n      onTap: () async {\n        try {\n          if (type == 'ketang') {\n            SmartDialog.showToast('课堂视频暂不支持播放');\n            return;\n          }\n          if (showCharge && videoItem?.typeid == 33) {\n            final String redirectUrl = await UrlUtils.parseRedirectUrl(\n                '${HttpString.baseUrl}/video/$bvid/');\n            final String lastPathSegment = redirectUrl.split('/').last;\n            if (lastPathSegment.contains('ss')) {\n              RoutePush.bangumiPush(\n                  Utils.matchNum(lastPathSegment).first, null);\n            }\n            if (lastPathSegment.contains('ep')) {\n              RoutePush.bangumiPush(\n                  null, Utils.matchNum(lastPathSegment).first);\n            }\n            return;\n          }\n          final int cid =\n              videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);\n          Get.toNamed('/video?bvid=$bvid&cid=$cid',\n              arguments: {'videoItem': videoItem, 'heroTag': heroTag});\n        } catch (err) {\n          SmartDialog.showToast(err.toString());\n        }\n      },\n      onLongPress: () => imageSaveDialog(\n        context,\n        videoItem,\n        SmartDialog.dismiss,\n      ),\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(\n            StyleString.safeSpace, 5, StyleString.safeSpace, 5),\n        child: LayoutBuilder(\n          builder: (BuildContext context, BoxConstraints boxConstraints) {\n            final double width = (boxConstraints.maxWidth -\n                    StyleString.cardSpace *\n                        6 /\n                        MediaQuery.textScalerOf(context).scale(1.0)) /\n                2;\n            return Container(\n              constraints: const BoxConstraints(minHeight: 88),\n              height: width / StyleString.aspectRatio,\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.start,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: <Widget>[\n                  AspectRatio(\n                    aspectRatio: StyleString.aspectRatio,\n                    child: LayoutBuilder(\n                      builder: (BuildContext context,\n                          BoxConstraints boxConstraints) {\n                        final double maxWidth = boxConstraints.maxWidth;\n                        final double maxHeight = boxConstraints.maxHeight;\n                        return Stack(\n                          children: [\n                            Hero(\n                              tag: heroTag,\n                              child: NetworkImgLayer(\n                                src: videoItem.pic as String,\n                                width: maxWidth,\n                                height: maxHeight,\n                              ),\n                            ),\n                            if (videoItem.duration != 0)\n                              PBadge(\n                                text: Utils.timeFormat(videoItem.duration!),\n                                right: 6.0,\n                                bottom: 6.0,\n                                type: 'gray',\n                              ),\n                            if (type != 'video')\n                              PBadge(\n                                text: type,\n                                left: 6.0,\n                                bottom: 6.0,\n                                type: 'primary',\n                              ),\n                            // if (videoItem.rcmdReason != null &&\n                            //     videoItem.rcmdReason.content != '')\n                            //   pBadge(videoItem.rcmdReason.content, context,\n                            //       6.0, 6.0, null, null),\n                            if (showCharge && videoItem?.isChargingSrc)\n                              const PBadge(\n                                text: '充电专属',\n                                right: 6.0,\n                                top: 6.0,\n                                type: 'primary',\n                              ),\n                          ],\n                        );\n                      },\n                    ),\n                  ),\n                  VideoContent(\n                    videoItem: videoItem,\n                    source: source,\n                    showOwner: showOwner,\n                    showView: showView,\n                    showDanmaku: showDanmaku,\n                    showPubdate: showPubdate,\n                    onPressedFn: onPressedFn,\n                  )\n                ],\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass VideoContent extends StatelessWidget {\n  // ignore: prefer_typing_uninitialized_variables\n  final videoItem;\n  final String source;\n  final bool showOwner;\n  final bool showView;\n  final bool showDanmaku;\n  final bool showPubdate;\n  final Function()? onPressedFn;\n\n  const VideoContent({\n    super.key,\n    required this.videoItem,\n    this.source = 'normal',\n    this.showOwner = true,\n    this.showView = true,\n    this.showDanmaku = true,\n    this.showPubdate = false,\n    this.onPressedFn,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(10, 0, 6, 0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (source == 'normal' || source == 'later') ...[\n              Text(\n                videoItem.title as String,\n                textAlign: TextAlign.start,\n                style: const TextStyle(\n                  fontWeight: FontWeight.w500,\n                ),\n                maxLines: 2,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ] else ...[\n              RichText(\n                maxLines: 2,\n                text: TextSpan(\n                  children: [\n                    for (final i in videoItem.titleList) ...[\n                      TextSpan(\n                        text: i['text'] as String,\n                        style: TextStyle(\n                          fontWeight: FontWeight.w500,\n                          letterSpacing: 0.3,\n                          color: i['type'] == 'em'\n                              ? Theme.of(context).colorScheme.primary\n                              : Theme.of(context).colorScheme.onSurface,\n                        ),\n                      ),\n                    ]\n                  ],\n                ),\n              ),\n            ],\n            const Spacer(),\n            // if (videoItem.rcmdReason != null &&\n            //     videoItem.rcmdReason.content != '')\n            //   Container(\n            //     padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),\n            //     decoration: BoxDecoration(\n            //       borderRadius: BorderRadius.circular(4),\n            //       border: Border.all(\n            //           color: Theme.of(context).colorScheme.surfaceTint),\n            //     ),\n            //     child: Text(\n            //       videoItem.rcmdReason.content,\n            //       style: TextStyle(\n            //           fontSize: 9,\n            //           color: Theme.of(context).colorScheme.surfaceTint),\n            //     ),\n            //   ),\n            // const SizedBox(height: 4),\n            if (showPubdate)\n              Text(\n                Utils.dateFormat(videoItem.pubdate!),\n                style: TextStyle(\n                    fontSize: 11, color: Theme.of(context).colorScheme.outline),\n              ),\n            if (showOwner)\n              Row(\n                children: [\n                  Text(\n                    videoItem.owner.name as String,\n                    style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                  ),\n                ],\n              ),\n            Row(\n              children: [\n                if (showView) ...[\n                  StatView(view: videoItem.stat.view as int),\n                  const SizedBox(width: 8),\n                ],\n                if (showDanmaku)\n                  StatDanMu(danmu: videoItem.stat.danmaku as int),\n                const Spacer(),\n                if (source == 'normal')\n                  SizedBox(\n                    width: 24,\n                    height: 24,\n                    child: IconButton(\n                      padding: EdgeInsets.zero,\n                      onPressed: () {\n                        feedBack();\n                        showModalBottomSheet(\n                          context: context,\n                          useRootNavigator: true,\n                          isScrollControlled: true,\n                          builder: (context) {\n                            return MorePanel(videoItem: videoItem);\n                          },\n                        );\n                      },\n                      icon: Icon(\n                        Icons.more_vert_outlined,\n                        color: Theme.of(context).colorScheme.outline,\n                        size: 14,\n                      ),\n                    ),\n                  ),\n                if (source == 'later') ...[\n                  IconButton(\n                    style: ButtonStyle(\n                      padding: MaterialStateProperty.all(EdgeInsets.zero),\n                    ),\n                    onPressed: () => onPressedFn?.call(),\n                    icon: Icon(\n                      Icons.clear_outlined,\n                      color: Theme.of(context).colorScheme.outline,\n                      size: 18,\n                    ),\n                  )\n                ],\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass MorePanel extends StatelessWidget {\n  final dynamic videoItem;\n  const MorePanel({super.key, required this.videoItem});\n\n  Future<dynamic> menuActionHandler(String type) async {\n    switch (type) {\n      case 'block':\n        blockUser();\n        break;\n      case 'watchLater':\n        var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String);\n        SmartDialog.showToast(res['msg']);\n        Get.back();\n        break;\n      default:\n    }\n  }\n\n  void blockUser() async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'\n              '\\n\\n注：被拉黑的Up可以在隐私设置-黑名单管理中解除'),\n          actions: [\n            TextButton(\n              onPressed: () => SmartDialog.dismiss(),\n              child: Text(\n                '点错了',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                var res = await VideoHttp.relationMod(\n                  mid: videoItem.owner.mid,\n                  act: 5,\n                  reSrc: 11,\n                );\n                SmartDialog.dismiss();\n                SmartDialog.showToast(res['msg'] ?? '成功');\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          InkWell(\n            onTap: () => Get.back(),\n            child: Container(\n              height: 35,\n              padding: const EdgeInsets.only(bottom: 2),\n              child: Center(\n                child: Container(\n                  width: 32,\n                  height: 3,\n                  decoration: BoxDecoration(\n                      color: Theme.of(context).colorScheme.outline,\n                      borderRadius: const BorderRadius.all(Radius.circular(3))),\n                ),\n              ),\n            ),\n          ),\n          ListTile(\n            onTap: () async => await menuActionHandler('block'),\n            minLeadingWidth: 0,\n            leading: const Icon(Icons.block, size: 19),\n            title: Text(\n              '拉黑up主 「${videoItem.owner.name}」',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ),\n          ListTile(\n            onTap: () async => await menuActionHandler('watchLater'),\n            minLeadingWidth: 0,\n            leading: const Icon(Icons.watch_later_outlined, size: 19),\n            title:\n                Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall),\n          ),\n          ListTile(\n            onTap: () =>\n                imageSaveDialog(context, videoItem, SmartDialog.dismiss),\n            minLeadingWidth: 0,\n            leading: const Icon(Icons.photo_outlined, size: 19),\n            title:\n                Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall),\n          ),\n          const SizedBox(height: 20),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widgets/video_card_v.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/image_save.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport '../../models/model_rec_video_item.dart';\nimport 'stat/danmu.dart';\nimport 'stat/view.dart';\nimport '../../http/dynamics.dart';\nimport '../../http/user.dart';\nimport '../../http/video.dart';\nimport '../../utils/id_utils.dart';\nimport '../../utils/utils.dart';\nimport '../constants.dart';\nimport 'badge.dart';\nimport 'network_img_layer.dart';\n\n// 视频卡片 - 垂直布局\nclass VideoCardV extends StatelessWidget {\n  final dynamic videoItem;\n  final int crossAxisCount;\n  final Function? blockUserCb;\n\n  const VideoCardV({\n    Key? key,\n    required this.videoItem,\n    required this.crossAxisCount,\n    this.blockUserCb,\n  }) : super(key: key);\n\n  bool isStringNumeric(String str) {\n    RegExp numericRegex = RegExp(r'^\\d+$');\n    return numericRegex.hasMatch(str);\n  }\n\n  void onPushDetail(heroTag) async {\n    String goto = videoItem.goto;\n    switch (goto) {\n      case 'bangumi':\n        if (videoItem.bangumiBadge == '电影') {\n          SmartDialog.showToast('暂不支持电影观看');\n          return;\n        }\n        int epId = videoItem.param;\n        RoutePush.bangumiPush(\n          null,\n          epId,\n          heroTag: heroTag,\n        );\n        break;\n      case 'av':\n        String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);\n        Get.toNamed('/video?bvid=$bvid&cid=${videoItem.cid}', arguments: {\n          // 'videoItem': videoItem,\n          'pic': videoItem.pic,\n          'heroTag': heroTag,\n        });\n        break;\n      // 动态\n      case 'picture':\n        try {\n          String uri = videoItem.uri;\n          if (videoItem.uri.startsWith('bilibili://article/')) {\n            // https://www.bilibili.com/read/cv27063554\n            RegExp regex = RegExp(r'\\d+');\n            Match match = regex.firstMatch(videoItem.uri)!;\n            String matchedNumber = match.group(0)!;\n            videoItem.param = int.parse(matchedNumber);\n          }\n          if (uri.startsWith('http')) {\n            String path = Uri.parse(uri).path;\n            if (isStringNumeric(path.split('/')[1])) {\n              // 请求接口\n              var res =\n                  await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);\n              if (res['status']) {\n                Get.toNamed('/dynamicDetail', arguments: {\n                  'item': res['data'],\n                  'floor': 1,\n                  'action': 'detail'\n                });\n              }\n              return;\n            }\n          }\n          Get.toNamed('/read', parameters: {\n            'title': videoItem.title,\n            'id': videoItem.param,\n            'articleType': 'read'\n          });\n        } catch (err) {\n          SmartDialog.showToast(err.toString());\n        }\n        break;\n      default:\n        SmartDialog.showToast(videoItem.goto);\n        Get.toNamed(\n          '/webview',\n          parameters: {\n            'url': videoItem.uri,\n            'type': 'url',\n            'pageTitle': videoItem.title,\n          },\n        );\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(videoItem.id);\n    return InkWell(\n      onTap: () async => onPushDetail(heroTag),\n      onLongPress: () => imageSaveDialog(\n        context,\n        videoItem,\n        SmartDialog.dismiss,\n      ),\n      borderRadius: BorderRadius.circular(16),\n      child: Column(\n        children: [\n          AspectRatio(\n            aspectRatio: StyleString.aspectRatio,\n            child: LayoutBuilder(builder: (context, boxConstraints) {\n              double maxWidth = boxConstraints.maxWidth;\n              double maxHeight = boxConstraints.maxHeight;\n              return Stack(\n                children: [\n                  Hero(\n                    tag: heroTag,\n                    child: NetworkImgLayer(\n                      src: videoItem.pic,\n                      width: maxWidth,\n                      height: maxHeight,\n                    ),\n                  ),\n                  if (videoItem.duration > 0)\n                    if (crossAxisCount == 1) ...[\n                      PBadge(\n                        bottom: 10,\n                        right: 10,\n                        text: Utils.timeFormat(videoItem.duration),\n                      )\n                    ] else ...[\n                      PBadge(\n                        bottom: 6,\n                        right: 7,\n                        size: 'small',\n                        type: 'gray',\n                        text: Utils.timeFormat(videoItem.duration),\n                      )\n                    ],\n                ],\n              );\n            }),\n          ),\n          VideoContent(\n            videoItem: videoItem,\n            crossAxisCount: crossAxisCount,\n            blockUserCb: blockUserCb,\n          )\n        ],\n      ),\n    );\n  }\n}\n\nclass VideoContent extends StatelessWidget {\n  final dynamic videoItem;\n  final int crossAxisCount;\n  final Function? blockUserCb;\n\n  const VideoContent({\n    Key? key,\n    required this.videoItem,\n    required this.crossAxisCount,\n    this.blockUserCb,\n  }) : super(key: key);\n\n  Widget _buildBadge(String text, String type, [double fs = 12]) {\n    return PBadge(\n      text: text,\n      stack: 'normal',\n      size: 'small',\n      type: type,\n      fs: fs,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: crossAxisCount == 1\n          ? const EdgeInsets.fromLTRB(9, 9, 9, 4)\n          : const EdgeInsets.fromLTRB(5, 8, 5, 4),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Text(\n            videoItem.title,\n            maxLines: 2,\n            overflow: TextOverflow.ellipsis,\n          ),\n          if (crossAxisCount > 1) ...[\n            const SizedBox(height: 2),\n            VideoStat(videoItem: videoItem, crossAxisCount: crossAxisCount),\n          ],\n          if (crossAxisCount == 1) const SizedBox(height: 4),\n          Row(\n            children: [\n              if (videoItem.goto == 'bangumi')\n                _buildBadge(videoItem.bangumiBadge, 'line', 9),\n              if (videoItem.rcmdReason != null)\n                _buildBadge(videoItem.rcmdReason, 'color'),\n              if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9),\n              if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'),\n              Expanded(\n                flex: crossAxisCount == 1 ? 0 : 1,\n                child: Text(\n                  videoItem.owner.name,\n                  maxLines: 1,\n                  style: TextStyle(\n                    fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                    color: Theme.of(context).colorScheme.outline,\n                  ),\n                ),\n              ),\n              if (crossAxisCount == 1) ...[\n                const SizedBox(width: 10),\n                VideoStat(\n                  videoItem: videoItem,\n                  crossAxisCount: crossAxisCount,\n                ),\n                const Spacer(),\n              ],\n              if (videoItem.goto == 'av')\n                SizedBox(\n                  width: 24,\n                  height: 24,\n                  child: IconButton(\n                    padding: EdgeInsets.zero,\n                    onPressed: () {\n                      feedBack();\n                      showModalBottomSheet(\n                        context: context,\n                        useRootNavigator: true,\n                        isScrollControlled: true,\n                        builder: (context) {\n                          return MorePanel(\n                            videoItem: videoItem,\n                            blockUserCb: blockUserCb,\n                          );\n                        },\n                      );\n                    },\n                    icon: Icon(\n                      Icons.more_vert_outlined,\n                      color: Theme.of(context).colorScheme.outline,\n                      size: 14,\n                    ),\n                  ),\n                )\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass VideoStat extends StatelessWidget {\n  final dynamic videoItem;\n  final int crossAxisCount;\n\n  const VideoStat({\n    Key? key,\n    required this.videoItem,\n    required this.crossAxisCount,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        StatView(view: videoItem.stat.view),\n        const SizedBox(width: 8),\n        StatDanMu(danmu: videoItem.stat.danmu),\n        if (videoItem is RecVideoItemModel) ...<Widget>[\n          crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),\n          RichText(\n            maxLines: 1,\n            text: TextSpan(\n                style: TextStyle(\n                  fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n                text: Utils.formatTimestampToRelativeTime(videoItem.pubdate)),\n          ),\n          const SizedBox(width: 4),\n        ]\n      ],\n    );\n  }\n}\n\nclass MorePanel extends StatelessWidget {\n  final dynamic videoItem;\n  final Function? blockUserCb;\n  const MorePanel({\n    super.key,\n    required this.videoItem,\n    this.blockUserCb,\n  });\n\n  Future<dynamic> menuActionHandler(String type) async {\n    switch (type) {\n      case 'block':\n        Get.back();\n        blockUser();\n        break;\n      case 'watchLater':\n        var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String);\n        SmartDialog.showToast(res['msg']);\n        Get.back();\n        break;\n      default:\n    }\n  }\n\n  void blockUser() async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'\n              '\\n\\n注：被拉黑的Up可以在隐私设置-黑名单管理中解除'),\n          actions: [\n            TextButton(\n              onPressed: () => SmartDialog.dismiss(),\n              child: Text(\n                '点错了',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                var res = await VideoHttp.relationMod(\n                  mid: videoItem.owner.mid,\n                  act: 5,\n                  reSrc: 11,\n                );\n                SmartDialog.dismiss();\n                if (res['status']) {\n                  blockUserCb?.call(videoItem.owner.mid);\n                }\n                SmartDialog.showToast(res['msg']);\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          InkWell(\n            onTap: () => Get.back(),\n            child: Container(\n              height: 35,\n              padding: const EdgeInsets.only(bottom: 2),\n              child: Center(\n                child: Container(\n                  width: 32,\n                  height: 3,\n                  decoration: BoxDecoration(\n                      color: Theme.of(context).colorScheme.outline,\n                      borderRadius: const BorderRadius.all(Radius.circular(3))),\n                ),\n              ),\n            ),\n          ),\n          ListTile(\n            onTap: () async => await menuActionHandler('block'),\n            minLeadingWidth: 0,\n            leading: const Icon(Icons.block, size: 19),\n            title: Text(\n              '拉黑up主 「${videoItem.owner.name}」',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ),\n          ListTile(\n            onTap: () async => await menuActionHandler('watchLater'),\n            minLeadingWidth: 0,\n            leading: const Icon(Icons.watch_later_outlined, size: 19),\n            title:\n                Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall),\n          ),\n          ListTile(\n            onTap: () =>\n                imageSaveDialog(context, videoItem, SmartDialog.dismiss),\n            minLeadingWidth: 0,\n            leading: const Icon(Icons.photo_outlined, size: 19),\n            title:\n                Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall),\n          ),\n          const SizedBox(height: 20),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/http/api.dart",
    "content": "import 'constants.dart';\n\nclass Api {\n  // 推荐视频\n  static const String recommendListApp =\n      '${HttpString.appBaseUrl}/x/v2/feed/index';\n  static const String recommendListWeb = '/x/web-interface/index/top/feed/rcmd';\n\n  // 热门视频\n  static const String hotList = '/x/web-interface/popular';\n\n  // 视频流\n  // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md\n  static const String videoUrl = '/x/player/wbi/playurl';\n\n  // 视频详情\n  // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921\n  // https://api.bilibili.com/x/web-interface/view/detail  获取视频超详细信息(web端)\n  static const String videoIntro = '/x/web-interface/view';\n  // 视频详情 超详细\n  // https://api.bilibili.com/x/web-interface/view/detail?aid=527403921\n\n  /// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/action.md\n  // 点赞 Post\n  /// aid\tnum\t稿件avid\t必要（可选）\tavid与bvid任选一个\n  /// bvid\tstr\t稿件bvid\t必要（可选）\tavid与bvid任选一个\n  /// like\tnum\t操作方式\t必要\t1：点赞 2：取消赞\n  // csrf\tstr\tCSRF Token（位于cookie）\t必要\n  // https://api.bilibili.com/x/web-interface/archive/like\n  static const String likeVideo = '/x/web-interface/archive/like';\n\n  //判断视频是否被点赞（双端）Get\n  // access_key\tstr\tAPP登录Token\tAPP方式必要\n  /// aid\tnum\t稿件avid\t必要（可选）\tavid与bvid任选一个\n  /// bvid\tstr\t稿件bvid\t必要（可选）\tavid与bvid任选一个\n  // https://api.bilibili.com/x/web-interface/archive/has/like\n  static const String hasLikeVideo = '/x/web-interface/archive/has/like';\n\n  // 视频点踩 web端不支持\n\n  // 投币视频（web端）POST\n  /// aid\tnum\t稿件avid\t必要（可选）\tavid与bvid任选一个\n  /// bvid\tstr\t稿件bvid\t必要（可选）\tavid与bvid任选一个\n  /// multiply\tnum\t投币数量\t必要\t上限为2\n  /// select_like\tnum\t是否附加点赞\t非必要\t0：不点赞 1：同时点赞 默认为0\n  // csrf\tstr\tCSRF Token（位于cookie）\t必要\n  // https://api.bilibili.com/x/web-interface/coin/add\n  static const String coinVideo = '/x/web-interface/coin/add';\n\n  // 判断视频是否被投币（双端）GET\n  // access_key\tstr\tAPP登录Token\tAPP方式必要\n  /// aid\tnum\t稿件avid\t必要（可选）\tavid与bvid任选一个\n  /// bvid\tstr\t稿件bvid\t必要（可选）\tavid与bvid任选一个\n  /// https://api.bilibili.com/x/web-interface/archive/coins\n  static const String hasCoinVideo = '/x/web-interface/archive/coins';\n\n  // 收藏视频（双端）POST\n  // access_key\tstr\tAPP登录Token\tAPP方式必要\n  /// rid\tnum\t稿件avid\t必要\n  /// type\tnum\t必须为2\t必要\n  /// add_media_ids\tnums\t需要加入的收藏夹mlid\t非必要\t同时添加多个，用,（%2C）分隔\n  /// del_media_ids\tnums\t需要取消的收藏夹mlid\t非必要\t同时取消多个，用,（%2C）分隔\n  // csrf\tstr\tCSRF Token（位于cookie）\tCookie方式必要\n  // https://api.bilibili.com/medialist/gateway/coll/resource/deal\n  // https://api.bilibili.com/x/v3/fav/resource/deal\n  static const String favVideo = '/x/v3/fav/resource/deal';\n\n  // 判断视频是否被收藏（双端）GET\n  /// aid\n  // https://api.bilibili.com/x/v2/fav/video/favoured\n  static const String hasFavVideo = '/x/v2/fav/video/favoured';\n\n  // 分享视频 （Web端） POST\n  // https://api.bilibili.com/x/web-interface/share/add\n  // aid\tnum\t稿件avid\t必要（可选）\tavid与bvid任选一个\n  // bvid\tstr\t稿件bvid\t必要（可选）\tavid与bvid任选一个\n  // csrf\tstr\tCSRF Token（位于cookie）\t必要\n\n  // 一键三连\n  // https://api.bilibili.com/x/web-interface/archive/like/triple\n  // aid\tnum\t稿件avid\t必要（可选）\tavid与bvid任选一个\n  // bvid\tstr\t稿件bvid\t必要（可选）\tavid与bvid任选一个\n  // csrf\tstr\tCSRF Token（位于cookie）\t必要\n  static const String oneThree = '/x/web-interface/archive/like/triple';\n\n  // 获取指定用户创建的所有收藏夹信息\n  // 该接口也能查询目标内容id存在于那些收藏夹中\n  // up_mid\tnum\t目标用户mid\t必要\n  // type\tnum\t目标内容属性\t非必要\t默认为全部 0：全部 2：视频稿件\n  // rid\tnum\t目标 视频稿件avid\n  static const String videoInFolder = '/x/v3/fav/folder/created/list-all';\n\n  // 视频详情页 相关视频\n  static const String relatedList = '/x/web-interface/archive/related';\n\n  // 查询用户与自己关系_仅查关注\n  static const String hasFollow = '/x/relation';\n\n  // 操作用户关系\n  static const String relationMod = '/x/relation/modify';\n\n  // 相互关系查询 // 失效\n  // static const String relationSearch = '/x/space/wbi/acc/relation';\n\n  // 评论列表\n  // https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11\n  static const String replyList = '/x/v2/reply';\n\n  // 楼中楼\n  static const String replyReplyList = '/x/v2/reply/reply';\n\n  // 评论点赞\n  static const String likeReply = '/x/v2/reply/action';\n\n  // 发表评论\n  // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/comment/action.md\n  static const String replyAdd = '/x/v2/reply/add';\n\n  // 用户(被)关注数、投稿数\n  // https://api.bilibili.com/x/relation/stat?vmid=697166795\n  static const String userStat = '/x/relation/stat';\n\n  // 获取用户信息\n  static const String userInfo = '/x/web-interface/nav';\n\n  // 获取当前用户状态\n  static const String userStatOwner = '/x/web-interface/nav/stat';\n\n  // 收藏夹\n  // https://api.bilibili.com/x/v3/fav/folder/created/list?pn=1&ps=10&up_mid=17340771\n  static const String userFavFolder = '/x/v3/fav/folder/created/list';\n\n  /// 收藏夹 详情\n  /// media_id  当前收藏夹id 搜索全部时为默认收藏夹id\n  /// pn int 当前页\n  /// ps int pageSize\n  /// keyword String 搜索词\n  /// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿\n  /// tid int 分区id\n  /// platform web\n  /// type 0 当前收藏夹 1 全部收藏夹\n  // https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0\n  static const String userFavFolderDetail = '/x/v3/fav/resource/list';\n\n  // 正在直播的up & 关注的up\n  // https://api.bilibili.com/x/polymer/web-dynamic/v1/portal\n  static const String followUp = '/x/polymer/web-dynamic/v1/portal';\n\n  // 关注的up动态\n  // https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all\n  // https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all?timezone_offset=-480&type=video&page=1&features=itemOpusStyle\n  // https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all?host_mid=548196587&offset=&page=1&features=itemOpusStyle\n  static const String followDynamic = '/x/polymer/web-dynamic/v1/feed/all';\n\n  // 动态点赞\n  static const String likeDynamic =\n      '${HttpString.tUrl}/dynamic_like/v1/dynamic_like/thumb';\n\n  // 获取稍后再看\n  static const String seeYouLater = '/x/v2/history/toview';\n\n  // 获取历史记录\n  static const String historyList = '/x/web-interface/history/cursor';\n\n  // 暂停历史记录\n  static const String pauseHistory = '/x/v2/history/shadow/set';\n\n  // 查询历史记录暂停状态\n  static const String historyStatus = '/x/v2/history/shadow?jsonp=jsonp';\n\n  // 清空历史记录\n  static const String clearHistory = '/x/v2/history/clear';\n\n  // 删除某条历史记录\n  static const String delHistory = '/x/v2/history/delete';\n\n  // 搜索历史记录\n  static const String searchHistory = '/x/web-goblin/history/search';\n\n  // 热搜\n  static const String hotSearchList =\n      'https://s.search.bilibili.com/main/hotword';\n\n  // 默认搜索词\n  static const String searchDefault = '/x/web-interface/wbi/search/default';\n\n  // 搜索关键词\n  static const String searchSuggest =\n      'https://s.search.bilibili.com/main/suggest';\n\n  // 分类搜索\n  static const String searchByType = '/x/web-interface/wbi/search/type';\n\n  // 记录视频播放进度\n  // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md\n  static const String heartBeat = '/x/click-interface/web/heartbeat';\n\n  // 查询视频分P列表 (avid/bvid转cid)\n  static const String ab2c = '/x/player/pagelist';\n\n  // 番剧/剧集明细\n  static const String bangumiInfo = '/pgc/view/web/season';\n\n  // 全部关注的up\n  // vmid 用户id pn 页码 ps 每页个数，最大50 order: desc\n  // order_type 排序规则 最近访问传空，最常访问传 attention\n  static const String followings = '/x/relation/followings';\n\n  // 指定分类的关注\n  // https://api.bilibili.com/x/relation/tag?mid=17340771&tagid=-10&pn=1&ps=20\n  static const String tagFollowings = '/x/relation/tag';\n\n  // 关注分类\n  // https://api.bilibili.com/x/relation/tags\n  static const String followingsClass = '/x/relation/tags';\n\n  // 搜索follow\n  static const followSearch = '/x/relation/followings/search';\n\n  // 粉丝\n  // vmid 用户id pn 页码 ps 每页个数，最大50 order: desc\n  // order_type 排序规则 最近访问传空，最常访问传 attention\n  static const String fans = '/x/relation/fans';\n\n  // 直播\n  // ?page=1&page_size=30&platform=web\n  static const String liveList =\n      '${HttpString.liveBaseUrl}/xlive/web-interface/v1/second/getUserRecommend';\n\n  // 直播间详情\n  // cid roomId\n  // qn 80:流畅，150:高清，400:蓝光，10000:原画，20000:4K, 30000:杜比\n  static const String liveRoomInfo =\n      '${HttpString.liveBaseUrl}/xlive/web-room/v2/index/getRoomPlayInfo';\n\n  // 直播间详情 H5\n  static const String liveRoomInfoH5 =\n      '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getH5InfoByRoom';\n\n  // 用户信息 需要Wbi签名\n  // https://api.bilibili.com/x/space/wbi/acc/info?mid=503427686&token=&platform=web&web_location=1550101&w_rid=d709892496ce93e3d94d6d37c95bde91&wts=1689301482\n  static const String memberInfo = '/x/space/wbi/acc/info';\n\n  // 用户名片信息\n  static const String memberCardInfo = '/x/web-interface/card';\n\n  // 用户投稿\n  // https://api.bilibili.com/x/space/wbi/arc/search?\n  // mid=85754245&\n  // ps=30&\n  // tid=0&\n  // pn=1&\n  // keyword=&\n  // order=pubdate&\n  // platform=web&\n  // web_location=1550101&\n  // order_avoided=true&\n  // w_rid=d893cf98a4e010cf326373194a648360&\n  // wts=1689767832\n  static const String memberArchive = '/x/space/wbi/arc/search';\n\n  // 用户动态搜索\n  static const String memberDynamicSearch = '/x/space/dynamic/search';\n\n  // 用户动态\n  static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';\n\n  // 稍后再看\n  static const String toViewLater = '/x/v2/history/toview/add';\n\n  // 移除已观看\n  static const String toViewDel = '/x/v2/history/toview/del';\n\n  // 清空稍后再看\n  static const String toViewClear = '/x/v2/history/toview/clear';\n\n  // 追番\n  static const String bangumiAdd = '/pgc/web/follow/add';\n\n  // 取消追番\n  static const String bangumiDel = '/pgc/web/follow/del';\n\n  // 番剧列表\n  // https://api.bilibili.com/pgc/season/index/result?\n  // st=1&\n  // order=3\n  // season_version=-1  全部-1 正片1 电影2 其他3\n  // spoken_language_type=-1  全部-1 原生1 中文配音2\n  // area=-1&\n  // is_finish=-1&\n  // copyright=-1&\n  // season_status=-1&\n  // season_month=-1&\n  // year=-1&\n  // style_id=-1&\n  // sort=0&\n  // page=1&\n  // season_type=1&\n  // pagesize=20&\n  // type=1\n  static const String bangumiList =\n      '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';\n\n  // 我的订阅\n  static const String bangumiFollow =\n      '/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969';\n\n  // 黑名单\n  static const String blackLst = '/x/relation/blacks';\n\n  // 移除黑名单\n  static const String removeBlack = '/x/relation/modify';\n\n  // github 获取最新版\n  static const String latestApp =\n      'https://api.github.com/repos/guozhigq/pilipala/releases/latest';\n\n  // 多少人在看\n  // https://api.bilibili.com/x/player/online/total?aid=913663681&cid=1203559746&bvid=BV1MM4y1s7NZ&ts=56427838\n  static const String onlineTotal = '/x/player/online/total';\n\n  static const String webDanmaku = '/x/v2/dm/web/seg.so';\n\n  //发送视频弹幕\n  //https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/danmaku/action.md\n  static const String shootDanmaku = '/x/v2/dm/post';\n\n  // up主分组\n  static const String followUpTag = '/x/relation/tags';\n\n  // 设置Up主分组\n  // 0 添加至默认分组  否则使用,分割tagid\n  static const String addUsers = '/x/relation/tags/addUsers';\n\n  // 获取指定分组下的up\n  static const String followUpGroup = '/x/relation/tag';\n\n  /// 私聊\n  ///  'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?\n  /// session_type=1&\n  /// group_fold=1&\n  /// unfollow_fold=0&\n  /// sort_rule=2&\n  /// build=0&\n  /// mobi_app=web&\n  /// w_rid=8641d157fb9a9255eb2159f316ee39e2&\n  /// wts=1697305010\n\n  static const String sessionList =\n      '${HttpString.tUrl}/session_svr/v1/session_svr/get_sessions';\n\n  /// 私聊用户信息\n  /// uids\n  /// build=0&mobi_app=web\n  static const String sessionAccountList =\n      '${HttpString.tUrl}/account/v1/user/cards';\n\n  /// https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?\n  /// talker_id=400787461&\n  /// session_type=1&\n  /// size=20&\n  /// sender_device_id=1&\n  /// build=0&\n  /// mobi_app=web&\n  /// web_location=333.1296&\n  /// w_rid=cfe3bf58c9fe181bbf4dd6c75175e6b0&\n  /// wts=1697350697\n\n  static const String sessionMsg =\n      '${HttpString.tUrl}/svr_sync/v1/svr_sync/fetch_session_msgs';\n\n  /// 标记已读 POST\n  /// talker_id:\n  /// session_type: 1\n  /// ack_seqno: 920224140918926\n  /// build: 0\n  /// mobi_app: web\n  /// csrf_token:\n  /// csrf:\n  static const String updateAck =\n      '${HttpString.tUrl}/session_svr/v1/session_svr/update_ack';\n\n  // 获取某个动态详情\n  // timezone_offset=-480\n  // id=849312409672744983\n  // features=itemOpusStyle\n  static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';\n\n  // AI总结\n  /// https://api.bilibili.com/x/web-interface/view/conclusion/get?\n  /// bvid=BV1ju4y1s7kn&\n  /// cid=1296086601&\n  /// up_mid=4641697&\n  /// w_rid=1607c6c5a4a35a1297e31992220900ae&\n  /// wts=1697033079\n  static const String aiConclusion = '/x/web-interface/view/conclusion/get';\n\n  // captcha验证码\n  static const String getCaptcha =\n      '${HttpString.passBaseUrl}/x/passport-login/captcha?source=main_web';\n\n  // web端短信验证码\n  static const String webSmsCode =\n      '${HttpString.passBaseUrl}/x/passport-login/web/sms/send';\n\n  // web端验证码登录\n  static const String webSmsLogin =\n      '${HttpString.passBaseUrl}/x/passport-login/web/login/sms';\n\n  // web端密码登录\n  static const String loginInByWebPwd =\n      '${HttpString.passBaseUrl}/x/passport-login/web/login';\n\n  // web端二维码\n  static const String qrCodeApi =\n      '${HttpString.passBaseUrl}/x/passport-login/web/qrcode/generate';\n\n  // 扫码登录\n  static const String loginInByQrcode =\n      '${HttpString.passBaseUrl}/x/passport-login/web/qrcode/poll';\n\n  // app端短信验证码\n  static const String appSmsCode =\n      '${HttpString.passBaseUrl}/x/passport-login/sms/send';\n\n  // app端验证码登录\n\n  // 获取短信验证码\n  // static const String appSafeSmsCode =\n  //     'https://passport.bilibili.com/x/safecenter/common/sms/send';\n\n  /// app端密码登录\n  /// username\n  /// password\n  /// key\n  /// rhash\n  static const String loginInByPwdApi =\n      '${HttpString.passBaseUrl}/x/passport-login/oauth2/login';\n\n  /// 密码加密密钥\n  /// disable_rcmd\n  /// local_id\n  static const getWebKey = '${HttpString.passBaseUrl}/x/passport-login/web/key';\n\n  /// cookie转access_key\n  static const cookieToKey =\n      '${HttpString.passBaseUrl}/x/passport-tv-login/h5/qrcode/confirm';\n\n  /// 申请二维码(TV端)\n  static const getTVCode =\n      'https://passport.snm0516.aisee.tv/x/passport-tv-login/qrcode/auth_code';\n\n  ///扫码登录（TV端）\n  static const qrcodePoll =\n      '${HttpString.passBaseUrl}/x/passport-tv-login/qrcode/poll';\n\n  /// 置顶视频\n  static const getTopVideoApi = '/x/space/top/arc';\n\n  /// 主页 - 最近投币的视频\n  /// vmid\n  /// gaia_source = main_web\n  /// web_location\n  /// w_rid\n  /// wts\n  static const getRecentCoinVideoApi = '/x/space/coin/video';\n\n  /// 最近点赞的视频\n  static const getRecentLikeVideoApi = '/x/space/like/video';\n\n  /// 最近追番\n  static const getRecentBangumiApi = '/x/space/bangumi/follow/list';\n\n  /// 用户专栏\n  static const getMemberSeasonsApi = '/x/polymer/web-space/home/seasons_series';\n\n  /// 获赞数 播放数\n  /// mid\n  static const getMemberViewApi = '/x/space/upstat';\n\n  /// 查询某个专栏\n  /// mid\n  /// season_id\n  /// sort_reverse\n  /// page_num\n  /// page_size\n  static const getSeasonDetailApi =\n      '/x/polymer/web-space/seasons_archives_list';\n\n  static const getSeriesDetailApi = '/x/series/archives';\n\n  /// 获取未读动态数\n  static const getUnreadDynamic = '/x/web-interface/dynamic/entrance';\n\n  /// 用户动态主页\n  static const dynamicSpmPrefix = 'https://space.bilibili.com/1/dynamic';\n\n  /// 激活buvid3\n  static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';\n\n  /// 获取字幕配置\n  static const getSubtitleConfig = '/x/player/v2';\n\n  /// 我的订阅\n  static const userSubFolder = '/x/v3/fav/folder/collected/list';\n\n  /// 我的订阅详情 type 21\n  static const userSeasonList = '/x/space/fav/season/list';\n\n  /// 我的订阅详情 type 11\n  static const userResourceList = '/x/v3/fav/resource/list';\n\n  /// 表情\n  static const emojiList = '/x/emote/user/panel/web';\n\n  /// 已读标记\n  static const String ackSessionMsg =\n      '${HttpString.tUrl}/session_svr/v1/session_svr/update_ack';\n\n  /// 发送私信\n  static const String sendMsg = '${HttpString.tUrl}/web_im/v1/web_im/send_msg';\n\n  /// 排行榜\n  static const String getRankApi = \"/x/web-interface/ranking/v2\";\n\n  /// 取消订阅\n  static const String cancelSub = '/x/v3/fav/season/unfav';\n\n  /// 动态转发\n  static const String dynamicForwardUrl = '/x/dynamic/feed/create/submit_check';\n\n  /// 创建动态\n  static const String dynamicCreate = '/x/dynamic/feed/create/dyn';\n\n  /// 删除收藏夹\n  static const String delFavFolder = '/x/v3/fav/folder/del';\n\n  /// 搜索结果计数\n  static const String searchCount = '/x/web-interface/wbi/search/all/v2';\n\n  /// 关闭会话\n  static const String removeSession =\n      '${HttpString.tUrl}/session_svr/v1/session_svr/remove_session';\n\n  /// 消息未读数\n  static const String unread = '${HttpString.tUrl}/x/im/web/msgfeed/unread';\n\n  /// 回复我的\n  static const String messageReplyAPi = '/x/msgfeed/reply';\n\n  /// 收到的赞\n  static const String messageLikeAPi = '/x/msgfeed/like';\n\n  /// 系统通知\n  static const String messageSystemAPi =\n      '${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify';\n\n  /// 系统通知 个人\n  static const String userMessageSystemAPi =\n      '${HttpString.messageBaseUrl}/x/sys-msg/query_user_notify';\n\n  /// 系统通知标记已读\n  static const String systemMarkRead =\n      '${HttpString.messageBaseUrl}/x/sys-msg/update_cursor';\n\n  /// 编辑收藏夹\n  static const String editFavFolder = '/x/v3/fav/folder/edit';\n\n  /// 新建收藏夹\n  static const String addFavFolder = '/x/v3/fav/folder/add';\n\n  /// 直播间弹幕信息\n  static const String getDanmuInfo =\n      '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo';\n\n  /// 直播间发送弹幕\n  static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send';\n\n  /// 我的关注 - 正在直播\n  static const String getFollowingLive =\n      '${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';\n\n  /// 稍后再看&收藏夹视频列表\n  static const String mediaList = '/x/v2/medialist/resource/list';\n\n  /// 用户专栏\n  static const String opusList = '/x/polymer/web-dynamic/v1/opus/feed/space';\n\n  ///\n  static const String getViewInfo = '/x/article/viewinfo';\n\n  /// 直播间记录\n  static const String liveRoomEntry =\n      '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/roomEntryAction';\n\n  /// 删除评论\n  static const String replyDel = '/x/v2/reply/del';\n}\n"
  },
  {
    "path": "lib/http/bangumi.dart",
    "content": "import '../models/bangumi/list.dart';\nimport 'index.dart';\n\nclass BangumiHttp {\n  static Future bangumiList({int? page}) async {\n    var res = await Request().get(Api.bangumiList, data: {'page': page});\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': BangumiListDataModel.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future bangumiFollow({int? mid}) async {\n    var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid});\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': BangumiListDataModel.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/black.dart",
    "content": "import '../models/user/black.dart';\nimport 'index.dart';\n\nclass BlackHttp {\n  static Future blackList({required int pn, int? ps}) async {\n    var res = await Request().get(Api.blackLst, data: {\n      'pn': pn,\n      'ps': ps ?? 50,\n      're_version': 0,\n      'jsonp': 'jsonp',\n      'csrf': await Request.getCsrf(),\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': BlackListDataModel.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 移除黑名单\n  static Future removeBlack({required int fid}) async {\n    var res = await Request().post(\n      Api.removeBlack,\n      data: {\n        'act': 6,\n        'csrf': await Request.getCsrf(),\n        'fid': fid,\n        'jsonp': 'jsonp',\n        're_src': 116,\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': [],\n        'msg': '操作成功',\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/common.dart",
    "content": "import 'index.dart';\n\nclass CommonHttp {\n  static Future unReadDynamic() async {\n    var res = await Request().get(Api.getUnreadDynamic,\n        data: {'alltype_offset': 0, 'video_offset': '', 'article_offset': 0});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']['dyn_basic_infos']};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/constants.dart",
    "content": "class HttpString {\n  static const String baseUrl = 'https://www.bilibili.com';\n  static const String apiBaseUrl = 'https://api.bilibili.com';\n  static const String tUrl = 'https://api.vc.bilibili.com';\n  static const String appBaseUrl = 'https://app.bilibili.com';\n  static const String liveBaseUrl = 'https://api.live.bilibili.com';\n  static const String passBaseUrl = 'https://passport.bilibili.com';\n  static const String messageBaseUrl = 'https://message.bilibili.com';\n  static const String bangumiBaseUrl = 'https://bili.meark.me';\n  static const List<int> validateStatusCodes = [\n    302,\n    304,\n    307,\n    400,\n    401,\n    403,\n    404,\n    405,\n    409,\n    412,\n    500,\n    503,\n    504,\n    509,\n    616,\n    617,\n    625,\n    626,\n    628,\n    629,\n    632,\n    643,\n    650,\n    652,\n    658,\n    662,\n    688,\n    689,\n    701,\n    799,\n    8888\n  ];\n}\n"
  },
  {
    "path": "lib/http/danmaku.dart",
    "content": "import 'package:dio/dio.dart';\nimport '../models/danmaku/dm.pb.dart';\nimport 'index.dart';\n\nclass DanmakaHttp {\n  // 获取视频弹幕\n  static Future queryDanmaku({\n    required int cid,\n    required int segmentIndex,\n  }) async {\n    // 构建参数对象\n    Map<String, int> params = {\n      'type': 1,\n      'oid': cid,\n      'segment_index': segmentIndex,\n    };\n    var response = await Request().get(\n      Api.webDanmaku,\n      data: params,\n      extra: {'resType': ResponseType.bytes},\n    );\n    return DmSegMobileReply.fromBuffer(response.data);\n  }\n\n  static Future shootDanmaku({\n    int type = 1, //弹幕类选择(1：视频弹幕 2：漫画弹幕)\n    required int oid, // 视频cid\n    required String msg, //弹幕文本(长度小于 100 字符)\n    int mode =\n        1, // 弹幕类型(1：滚动弹幕 4：底端弹幕 5：顶端弹幕 6：逆向弹幕(不能使用） 7：高级弹幕 8：代码弹幕（不能使用） 9：BAS弹幕（pool必须为2）)\n    // String? aid,// 稿件avid\n    // String? bvid,// bvid与aid必须有一个\n    required String bvid,\n    int? progress, // 弹幕出现在视频内的时间（单位为毫秒，默认为0）\n    int? color, // 弹幕颜色(默认白色，16777215）\n    int? fontsize, // 弹幕字号（默认25）\n    int? pool, // 弹幕池选择（0：普通池 1：字幕池 2：特殊池（代码/BAS弹幕）默认普通池，0）\n    //int? rnd,// 当前时间戳*1000000（若无此项，则发送弹幕冷却时间限制为90s；若有此项，则发送弹幕冷却时间限制为5s）\n    int? colorful, //60001：专属渐变彩色（需要会员）\n    int? checkbox_type, //是否带 UP 身份标识（0：普通；4：带有标识）\n    // String? csrf,//CSRF Token（位于 Cookie）\tCookie 方式必要\n    // String? access_key,//\tAPP 登录 Token\t\tAPP 方式必要\n  }) async {\n    // 构建参数对象\n    // assert(aid != null || bvid != null);\n    // assert(csrf != null || access_key != null);\n    assert(msg.length < 100);\n    // 构建参数对象\n    var params = <String, dynamic>{\n      'type': type,\n      'oid': oid,\n      'msg': msg,\n      'mode': mode,\n      //'aid': aid,\n      'bvid': bvid,\n      'progress': progress,\n      'color': color,\n      'fontsize': fontsize,\n      'pool': pool,\n      'rnd': DateTime.now().microsecondsSinceEpoch,\n      'colorful': colorful,\n      'checkbox_type': checkbox_type,\n      'csrf': await Request.getCsrf(),\n      // 'access_key': access_key,\n    }..removeWhere((key, value) => value == null);\n\n    var response = await Request().post(\n      Api.shootDanmaku,\n      data: params,\n    );\n    if (response.statusCode != 200) {\n      return {\n        'status': false,\n        'data': [],\n        'msg': '弹幕发送失败，状态码:${response.statusCode}',\n      };\n    }\n    if (response.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': response.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': \"${response.data['code']}: ${response.data['message']}\",\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/dynamics.dart",
    "content": "import 'dart:math';\nimport 'package:dio/dio.dart';\nimport '../models/dynamics/result.dart';\nimport '../models/dynamics/up.dart';\nimport 'index.dart';\n\nclass DynamicsHttp {\n  static Future followDynamic({\n    String? type,\n    int? page,\n    String? offset,\n    int? mid,\n  }) async {\n    Map<String, dynamic> data = {\n      'type': type ?? 'all',\n      'page': page ?? 1,\n      'timezone_offset': '-480',\n      'offset': page == 1 ? '' : offset,\n      'features': 'itemOpusStyle'\n    };\n    if (mid != -1) {\n      data['host_mid'] = mid;\n      data.remove('timezone_offset');\n    }\n    var res = await Request().get(Api.followDynamic, data: data);\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': DynamicsDataModel.fromJson(res.data['data']),\n        };\n      } catch (err) {\n        print(err);\n        return {\n          'status': false,\n          'data': [],\n          'msg': err.toString(),\n        };\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n        'code': res.data['code'],\n      };\n    }\n  }\n\n  static Future followUp() async {\n    var res = await Request().get(Api.followUp);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': FollowUpModel.fromJson(res.data['data']),\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 动态点赞\n  static Future likeDynamic({\n    required String? dynamicId,\n    required int? up,\n  }) async {\n    var res = await Request().post(\n      Api.likeDynamic,\n      data: {\n        'dynamic_id': dynamicId,\n        'up': up,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  //\n  static Future dynamicDetail({\n    String? id,\n  }) async {\n    var res = await Request().get(Api.dynamicDetail, data: {\n      'timezone_offset': -480,\n      'id': id,\n      'features': 'itemOpusStyle',\n    });\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': DynamicItemModel.fromJson(res.data['data']['item']),\n        };\n      } catch (err) {\n        return {\n          'status': false,\n          'data': [],\n          'msg': err.toString(),\n        };\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future dynamicForward() async {\n    var res = await Request().post(\n      Api.dynamicForwardUrl,\n      queryParameters: {\n        'csrf': await Request.getCsrf(),\n        'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'},\n        'x-bili-web-req-json': {'spm_id': '333.999'},\n      },\n      data: {\n        'attach_card': null,\n        'scene': 4,\n        'content': {\n          'conetents': [\n            {'raw_text': \"2\", 'type': 1, 'biz_id': \"\"}\n          ]\n        }\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future dynamicCreate({\n    required int mid,\n    required int scene,\n    int? oid,\n    String? dynIdStr,\n    String? rawText,\n  }) async {\n    DateTime now = DateTime.now();\n    int timestamp = now.millisecondsSinceEpoch ~/ 1000;\n    Random random = Random();\n    int randomNumber = random.nextInt(9000) + 1000;\n    String uploadId = '${mid}_${timestamp}_$randomNumber';\n\n    Map<String, dynamic> webRepostSrc = {\n      'dyn_id_str': dynIdStr ?? '',\n    };\n\n    /// 投稿转发\n    if (scene == 5) {\n      webRepostSrc = {\n        'revs_id': {'dyn_type': 8, 'rid': oid}\n      };\n    }\n    var res = await Request().post(\n      Api.dynamicCreate,\n      queryParameters: {\n        'platform': 'web',\n        'csrf': await Request.getCsrf(),\n        'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'},\n        'x-bili-web-req-json': {'spm_id': '333.999'},\n      },\n      data: {\n        'dyn_req': {\n          'content': {\n            'contents': [\n              {'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''}\n            ]\n          },\n          'scene': scene,\n          'attach_card': null,\n          'upload_id': uploadId,\n          'meta': {\n            'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'}\n          }\n        },\n        'web_repost_src': webRepostSrc\n      },\n      options: Options(contentType: 'application/json'),\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/fan.dart",
    "content": "import '../models/fans/result.dart';\nimport 'index.dart';\n\nclass FanHttp {\n  static Future fans({int? vmid, int? pn, int? ps, String? orderType}) async {\n    var res = await Request().get(Api.fans, data: {\n      'vmid': vmid,\n      'pn': pn,\n      'ps': ps,\n      'order': 'desc',\n      'order_type': orderType,\n    });\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': FansDataModel.fromJson(res.data['data'])};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/fav.dart",
    "content": "import 'index.dart';\n\nclass FavHttp {\n  /// 编辑收藏夹\n  static Future editFolder({\n    required String title,\n    required String intro,\n    required String mediaId,\n    String? cover,\n    int? privacy,\n  }) async {\n    var res = await Request().post(\n      Api.editFavFolder,\n      data: {\n        'title': title,\n        'intro': intro,\n        'media_id': mediaId,\n        'cover': cover ?? '',\n        'privacy': privacy ?? 0,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  /// 新建收藏夹\n  static Future addFolder({\n    required String title,\n    required String intro,\n    String? cover,\n    int? privacy,\n  }) async {\n    var res = await Request().post(\n      Api.addFavFolder,\n      data: {\n        'title': title,\n        'intro': intro,\n        'cover': cover ?? '',\n        'privacy': privacy ?? 0,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/follow.dart",
    "content": "import '../models/follow/result.dart';\nimport 'index.dart';\n\nclass FollowHttp {\n  static Future followings(\n      {int? vmid, int? pn, int? ps, String? orderType}) async {\n    var res = await Request().get(Api.followings, data: {\n      'vmid': vmid,\n      'pn': pn,\n      'ps': ps,\n      'order': 'desc',\n      'order_type': orderType,\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': FollowDataModel.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/html.dart",
    "content": "import 'package:html/dom.dart';\nimport 'package:html/parser.dart';\nimport 'index.dart';\n\nclass HtmlHttp {\n  // article\n  static Future reqHtml(id, dynamicType) async {\n    var response = await Request().get(\n      \"https://www.bilibili.com/opus/$id\",\n      extra: {'ua': 'pc'},\n    );\n\n    if (response.data.contains('Redirecting to')) {\n      RegExp regex = RegExp(r'//([\\w\\.]+)/(\\w+)/(\\w+)');\n      Match match = regex.firstMatch(response.data)!;\n      String matchedString = match.group(0)!;\n      response = await Request().get(\n        'https:$matchedString/',\n        extra: {'ua': 'pc'},\n      );\n    }\n    try {\n      Document rootTree = parse(response.data);\n      // log(response.data.body.toString());\n      Element body = rootTree.body!;\n      Element appDom = body.querySelector('#app')!;\n      Element authorHeader = appDom.querySelector('.fixed-author-header')!;\n      // 头像\n      String avatar = authorHeader.querySelector('img')!.attributes['src']!;\n      avatar = 'https:${avatar.split('@')[0]}';\n      String uname = authorHeader\n          .querySelector('.fixed-author-header__author__name')!\n          .text;\n\n      // 动态详情\n      Element opusDetail = appDom.querySelector('.opus-detail')!;\n      // 发布时间\n      String updateTime =\n          opusDetail.querySelector('.opus-module-author__pub__text')!.text;\n      //\n      String opusContent =\n          opusDetail.querySelector('.opus-module-content')!.innerHtml;\n      String? test;\n      try {\n        test = opusDetail\n            .querySelector('.horizontal-scroll-album__pic__img')!\n            .innerHtml;\n      } catch (_) {}\n\n      String commentId = opusDetail\n          .querySelector('.bili-comment-container')!\n          .className\n          .split(' ')[1]\n          .split('-')[2];\n      // List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');\n      return {\n        'status': true,\n        'avatar': avatar,\n        'uname': uname,\n        'updateTime': updateTime,\n        'content': (test ?? '') + opusContent,\n        'commentId': int.parse(commentId)\n      };\n    } catch (err) {\n      print('err: $err');\n    }\n  }\n\n  // read\n  static Future reqReadHtml(id, dynamicType) async {\n    var response = await Request().get(\n      \"https://www.bilibili.com/$dynamicType/$id/\",\n      extra: {'ua': 'pc'},\n    );\n    Document rootTree = parse(response.data);\n    Element body = rootTree.body!;\n    Element appDom = body.querySelector('#app')!;\n    Element authorHeader = appDom.querySelector('.up-left')!;\n    // 头像\n    // String avatar =\n    //     authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;\n    // print(avatar);\n    // avatar = 'https:${avatar.split('@')[0]}';\n    String uname = authorHeader.querySelector('.up-name')!.text.trim();\n    // 动态详情\n    Element opusDetail = appDom.querySelector('.article-content')!;\n    // 发布时间\n    // String updateTime =\n    //     opusDetail.querySelector('.opus-module-author__pub__text')!.text;\n    // print(updateTime);\n\n    //\n    String opusContent =\n        opusDetail.querySelector('#read-article-holder')!.innerHtml;\n    RegExp digitRegExp = RegExp(r'\\d+');\n    Iterable<Match> matches = digitRegExp.allMatches(id);\n    String number = matches.first.group(0)!;\n    return {\n      'status': true,\n      'avatar': '',\n      'uname': uname,\n      'updateTime': '',\n      'content': opusContent,\n      'commentId': int.parse(number)\n    };\n  }\n}\n"
  },
  {
    "path": "lib/http/index.dart",
    "content": "export 'api.dart';\nexport 'init.dart';\n"
  },
  {
    "path": "lib/http/init.dart",
    "content": "// ignore_for_file: avoid_print\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:developer';\nimport 'dart:io';\nimport 'dart:math' show Random;\nimport 'package:cookie_jar/cookie_jar.dart';\nimport 'package:dio/dio.dart';\nimport 'package:dio/io.dart';\nimport 'package:dio_cookie_manager/dio_cookie_manager.dart';\n// import 'package:dio_http2_adapter/dio_http2_adapter.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport '../utils/storage.dart';\nimport '../utils/utils.dart';\nimport 'api.dart';\nimport 'constants.dart';\nimport 'interceptor.dart';\n\nclass Request {\n  static final Request _instance = Request._internal();\n  static late CookieManager cookieManager;\n  static late final Dio dio;\n  factory Request() => _instance;\n  Box setting = GStrorage.setting;\n  static Box localCache = GStrorage.localCache;\n  late bool enableSystemProxy;\n  late String systemProxyHost;\n  late String systemProxyPort;\n  static final RegExp spmPrefixExp =\n      RegExp(r'<meta name=\"spm_prefix\" content=\"([^\"]+?)\">');\n  static String? buvid;\n\n  /// 设置cookie\n  static setCookie() async {\n    Box userInfoCache = GStrorage.userInfo;\n    Box setting = GStrorage.setting;\n    final String cookiePath = await Utils.getCookiePath();\n    final PersistCookieJar cookieJar = PersistCookieJar(\n      ignoreExpires: true,\n      storage: FileStorage(cookiePath),\n    );\n    cookieManager = CookieManager(cookieJar);\n    dio.interceptors.add(cookieManager);\n    final List<Cookie> cookie = await cookieManager.cookieJar\n        .loadForRequest(Uri.parse(HttpString.baseUrl));\n    final userInfo = userInfoCache.get('userInfoCache');\n    if (userInfo != null && userInfo.mid != null) {\n      final List<Cookie> cookie2 = await cookieManager.cookieJar\n          .loadForRequest(Uri.parse(HttpString.tUrl));\n      if (cookie2.isEmpty) {\n        try {\n          await Request().get(HttpString.tUrl);\n        } catch (e) {\n          log(\"setCookie, ${e.toString()}\");\n        }\n      }\n    }\n    setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);\n    String baseUrlType = 'default';\n    if (setting.get(SettingBoxKey.enableGATMode, defaultValue: false)) {\n      baseUrlType = 'bangumi';\n    }\n    setBaseUrl(type: baseUrlType);\n    try {\n      await buvidActivate();\n    } catch (e) {\n      log(\"setCookie, ${e.toString()}\");\n    }\n\n    final String cookieString = cookie\n        .map((Cookie cookie) => '${cookie.name}=${cookie.value}')\n        .join('; ');\n\n    dio.options.headers['cookie'] = cookieString;\n  }\n\n  // 从cookie中获取 csrf token\n  static Future<String> getCsrf() async {\n    List<Cookie> cookies = await cookieManager.cookieJar\n        .loadForRequest(Uri.parse(HttpString.apiBaseUrl));\n    String token = '';\n    if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {\n      token = cookies.firstWhere((e) => e.name == 'bili_jct').value;\n    }\n    return token;\n  }\n\n  static Future<String> getBuvid() async {\n    if (buvid != null) {\n      return buvid!;\n    }\n\n    final List<Cookie> cookies = await cookieManager.cookieJar\n        .loadForRequest(Uri.parse(HttpString.baseUrl));\n    buvid = cookies.firstWhere((cookie) => cookie.name == 'buvid3').value;\n    if (buvid == null) {\n      try {\n        var result = await Request().get(\n          \"${HttpString.apiBaseUrl}/x/frontend/finger/spi\",\n        );\n        buvid = result[\"data\"][\"b_3\"].toString();\n      } catch (e) {\n        // 处理请求错误\n        buvid = '';\n        print(\"Error fetching buvid: $e\");\n      }\n    }\n\n    return buvid!;\n  }\n\n  static setOptionsHeaders(userInfo, bool status) {\n    if (status) {\n      dio.options.headers['x-bili-mid'] = userInfo.mid.toString();\n      dio.options.headers['x-bili-aurora-eid'] =\n          IdUtils.genAuroraEid(userInfo.mid);\n    }\n    dio.options.headers['env'] = 'prod';\n    dio.options.headers['app-key'] = 'android64';\n    dio.options.headers['x-bili-aurora-zone'] = 'sh001';\n    dio.options.headers['referer'] = 'https://www.bilibili.com/';\n  }\n\n  static Future buvidActivate() async {\n    var html = await Request().get(Api.dynamicSpmPrefix);\n    String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!;\n    Random rand = Random();\n    String rand_png_end = base64.encode(\n        List<int>.generate(32, (_) => rand.nextInt(256)) +\n            List<int>.filled(4, 0) +\n            [73, 69, 78, 68] +\n            List<int>.generate(4, (_) => rand.nextInt(256)));\n\n    String jsonData = json.encode({\n      '3064': 1,\n      '39c8': '${spmPrefix}.fp.risk',\n      '3c43': {\n        'adca': 'Linux',\n        'bfe9': rand_png_end.substring(rand_png_end.length - 50),\n      },\n    });\n\n    await Request().post(Api.activateBuvidApi,\n        data: {'payload': jsonData},\n        options: Options(contentType: 'application/json'));\n  }\n\n  /*\n   * config it and create\n   */\n  Request._internal() {\n    //BaseOptions、Options、RequestOptions 都可以配置参数，优先级别依次递增，且可以根据优先级别覆盖参数\n    BaseOptions options = BaseOptions(\n      //请求基地址,可以包含子路径\n      baseUrl: HttpString.apiBaseUrl,\n      //连接服务器超时时间，单位是毫秒.\n      connectTimeout: const Duration(milliseconds: 12000),\n      //响应流上前后两次接受到数据的间隔，单位为毫秒。\n      receiveTimeout: const Duration(milliseconds: 12000),\n      //Http请求头.\n      headers: {},\n    );\n\n    enableSystemProxy = setting.get(SettingBoxKey.enableSystemProxy,\n        defaultValue: false) as bool;\n    systemProxyHost =\n        localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');\n    systemProxyPort =\n        localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');\n\n    dio = Dio(options);\n\n    /// fix 第三方登录 302重定向 跟iOS代理问题冲突\n    // ..httpClientAdapter = Http2Adapter(\n    //   ConnectionManager(\n    //     idleTimeout: const Duration(milliseconds: 10000),\n    //     onClientCreate: (_, ClientSetting config) =>\n    //         config.onBadCertificate = (_) => true,\n    //   ),\n    // );\n\n    /// 设置代理\n    if (enableSystemProxy) {\n      dio.httpClientAdapter = IOHttpClientAdapter(\n        createHttpClient: () {\n          final HttpClient client = HttpClient();\n          // Config the client.\n          client.findProxy = (Uri uri) {\n            // return 'PROXY host:port';\n            return 'PROXY $systemProxyHost:$systemProxyPort';\n          };\n          client.badCertificateCallback =\n              (X509Certificate cert, String host, int port) => true;\n          return client;\n        },\n      );\n    }\n\n    //添加拦截器\n    dio.interceptors.add(ApiInterceptor());\n\n    // 日志拦截器 输出请求、响应内容\n    dio.interceptors.add(LogInterceptor(\n      request: false,\n      requestHeader: false,\n      responseHeader: false,\n    ));\n\n    dio.transformer = BackgroundTransformer();\n    dio.options.validateStatus = (int? status) {\n      return status! >= 200 && status < 300 ||\n          HttpString.validateStatusCodes.contains(status);\n    };\n  }\n\n  /*\n   * get请求\n   */\n  get(url, {data, options, cancelToken, extra}) async {\n    Response response;\n    final Options options = Options();\n    ResponseType resType = ResponseType.json;\n    if (extra != null) {\n      resType = extra!['resType'] ?? ResponseType.json;\n      if (extra['ua'] != null) {\n        options.headers = {'user-agent': headerUa(type: extra['ua'])};\n      }\n    }\n    options.responseType = resType;\n\n    try {\n      response = await dio.get(\n        url,\n        queryParameters: data,\n        options: options,\n        cancelToken: cancelToken,\n      );\n      return response;\n    } on DioException catch (e) {\n      Response errResponse = Response(\n        data: {\n          'message': await ApiInterceptor.dioError(e)\n        }, // 将自定义 Map 数据赋值给 Response 的 data 属性\n        statusCode: 200,\n        requestOptions: RequestOptions(),\n      );\n      return errResponse;\n    }\n  }\n\n  /*\n   * post请求\n   */\n  post(url, {data, queryParameters, options, cancelToken, extra}) async {\n    // print('post-data: $data');\n    Response response;\n    try {\n      response = await dio.post(\n        url,\n        data: data,\n        queryParameters: queryParameters,\n        options:\n            options ?? Options(contentType: Headers.formUrlEncodedContentType),\n        cancelToken: cancelToken,\n      );\n      // print('post success: ${response.data}');\n      return response;\n    } on DioException catch (e) {\n      Response errResponse = Response(\n        data: {\n          'message': await ApiInterceptor.dioError(e)\n        }, // 将自定义 Map 数据赋值给 Response 的 data 属性\n        statusCode: 200,\n        requestOptions: RequestOptions(),\n      );\n      return errResponse;\n    }\n  }\n\n  /*\n   * 下载文件\n   */\n  downloadFile(urlPath, savePath) async {\n    Response response;\n    try {\n      response = await dio.download(urlPath, savePath,\n          onReceiveProgress: (int count, int total) {\n        //进度\n        // print(\"$count $total\");\n      });\n      print('downloadFile success: ${response.data}');\n\n      return response.data;\n    } on DioException catch (e) {\n      print('downloadFile error: $e');\n      return Future.error(ApiInterceptor.dioError(e));\n    }\n  }\n\n  /*\n   * 取消请求\n   *\n   * 同一个cancel token 可以用于多个请求，当一个cancel token取消时，所有使用该cancel token的请求都会被取消。\n   * 所以参数可选\n   */\n  void cancelRequests(CancelToken token) {\n    token.cancel(\"cancelled\");\n  }\n\n  String headerUa({type = 'mob'}) {\n    String headerUa = '';\n    if (type == 'mob') {\n      if (Platform.isIOS) {\n        headerUa =\n            'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1';\n      } else {\n        headerUa =\n            'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36';\n      }\n    } else {\n      headerUa =\n          'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';\n    }\n    return headerUa;\n  }\n\n  static setBaseUrl({String type = 'default'}) {\n    switch (type) {\n      case 'default':\n        dio.options.baseUrl = HttpString.apiBaseUrl;\n        break;\n      case 'bangumi':\n        dio.options.baseUrl = HttpString.bangumiBaseUrl;\n        break;\n      default:\n        dio.options.baseUrl = HttpString.apiBaseUrl;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/interceptor.dart",
    "content": "// ignore_for_file: avoid_print\n\nimport 'package:connectivity_plus/connectivity_plus.dart';\nimport 'package:dio/dio.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:hive/hive.dart';\nimport '../utils/storage.dart';\n\nclass ApiInterceptor extends Interceptor {\n  @override\n  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {\n    // print(\"请求之前\");\n    // 在请求之前添加头部或认证信息\n    // options.headers['Authorization'] = 'Bearer token';\n    // options.headers['Content-Type'] = 'application/json';\n    handler.next(options);\n  }\n\n  @override\n  void onResponse(Response response, ResponseInterceptorHandler handler) {\n    try {\n      if (response.statusCode == 302) {\n        final List<String> locations = response.headers['location']!;\n        if (locations.isNotEmpty) {\n          if (locations.first.startsWith('https://www.mcbbs.net')) {\n            final Uri uri = Uri.parse(locations.first);\n            final String? accessKey = uri.queryParameters['access_key'];\n            final String? mid = uri.queryParameters['mid'];\n            try {\n              Box localCache = GStrorage.localCache;\n              localCache.put(LocalCacheKey.accessKey,\n                  <String, String?>{'mid': mid, 'value': accessKey});\n            } catch (_) {}\n          }\n        }\n      }\n    } catch (err) {\n      print('ApiInterceptor: $err');\n    }\n\n    handler.next(response);\n  }\n\n  @override\n  void onError(DioException err, ErrorInterceptorHandler handler) async {\n    // 处理网络请求错误\n    // handler.next(err);\n    String url = err.requestOptions.uri.toString();\n    final excludedPatterns = RegExp(r'heartbeat|seg\\.so|online/total');\n    if (!excludedPatterns.hasMatch(url)) {\n      SmartDialog.showToast(\n        await dioError(err),\n        displayType: SmartToastType.onlyRefresh,\n      );\n    }\n    super.onError(err, handler);\n  }\n\n  static Future<String> dioError(DioException error) async {\n    switch (error.type) {\n      case DioExceptionType.badCertificate:\n        return '证书有误！';\n      case DioExceptionType.badResponse:\n        return '服务器异常，请稍后重试！';\n      case DioExceptionType.cancel:\n        return '请求已被取消，请重新请求';\n      case DioExceptionType.connectionError:\n        return '连接错误，请检查网络设置';\n      case DioExceptionType.connectionTimeout:\n        return '网络连接超时，请检查网络设置';\n      case DioExceptionType.receiveTimeout:\n        return '响应超时，请稍后重试！';\n      case DioExceptionType.sendTimeout:\n        return '发送请求超时，请检查网络设置';\n      case DioExceptionType.unknown:\n        final String res = await checkConnect();\n        return '$res，网络异常！';\n    }\n  }\n\n  static Future<String> checkConnect() async {\n    final List<ConnectivityResult> connectivityResult =\n        await Connectivity().checkConnectivity();\n    if (connectivityResult.contains(ConnectivityResult.mobile)) {\n      return '正在使用移动流量';\n    } else if (connectivityResult.contains(ConnectivityResult.wifi)) {\n      return '正在使用wifi';\n    } else if (connectivityResult.contains(ConnectivityResult.ethernet)) {\n      return '正在使用局域网';\n    } else if (connectivityResult.contains(ConnectivityResult.vpn)) {\n      return '正在使用代理网络';\n    } else if (connectivityResult.contains(ConnectivityResult.bluetooth)) {\n      return '正在使用蓝牙网络';\n    } else if (connectivityResult.contains(ConnectivityResult.other)) {\n      return '正在使用其他网络';\n    } else if (connectivityResult.contains(ConnectivityResult.none)) {\n      return '未连接到任何网络';\n    } else {\n      return '';\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/live.dart",
    "content": "import 'package:pilipala/models/live/follow.dart';\n\nimport '../models/live/item.dart';\nimport '../models/live/room_info.dart';\nimport '../models/live/room_info_h5.dart';\nimport 'api.dart';\nimport 'init.dart';\n\nclass LiveHttp {\n  static Future liveList(\n      {int? vmid, int? pn, int? ps, String? orderType}) async {\n    var res = await Request().get(Api.liveList,\n        data: {'page': pn, 'page_size': 30, 'platform': 'web'});\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data']['list']\n            .map<LiveItemModel>((e) => LiveItemModel.fromJson(e))\n            .toList()\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future liveRoomInfo({roomId, qn}) async {\n    var res = await Request().get(Api.liveRoomInfo, data: {\n      'room_id': roomId,\n      'protocol': '0, 1',\n      'format': '0, 1, 2',\n      'codec': '0, 1',\n      'qn': qn,\n      'platform': 'web',\n      'ptype': 8,\n      'dolby': 5,\n      'panorama': 1,\n    });\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': RoomInfoModel.fromJson(res.data['data'])};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future liveRoomInfoH5({roomId, qn}) async {\n    var res = await Request().get(Api.liveRoomInfoH5, data: {\n      'room_id': roomId,\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': RoomInfoH5Model.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 获取弹幕信息\n  static Future liveDanmakuInfo({roomId}) async {\n    var res = await Request().get(Api.getDanmuInfo, data: {\n      'id': roomId,\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 发送弹幕\n  static Future sendDanmaku({roomId, msg}) async {\n    var res = await Request().post(\n      Api.sendLiveMsg,\n      data: {\n        'bubble': 0,\n        'msg': msg,\n        'color': 16777215, // 颜色\n        'mode': 1, // 模式\n        'room_type': 0,\n        'jumpfrom': 71001, // 直播间来源\n        'reply_mid': 0,\n        'reply_attr': 0,\n        'replay_dmid': '',\n        'statistics': {\"appId\": 100, \"platform\": 5},\n        'fontsize': 25, // 字体大小\n        'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳\n        'roomid': roomId,\n        'csrf': await Request.getCsrf(),\n        'csrf_token': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 我的关注 正在直播\n  static Future liveFollowing({int? pn, int? ps}) async {\n    var res = await Request().get(Api.getFollowingLive, data: {\n      'page': pn,\n      'page_size': ps,\n      'platform': 'web',\n      'ignoreRecord': 1,\n      'hit_ab': true,\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': LiveFollowingModel.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 直播历史记录\n  static Future liveRoomEntry({required int roomId}) async {\n    await Request().post(\n      Api.liveRoomEntry,\n      data: {\n        'room_id': roomId,\n        'platform': 'pc',\n        'csrf_token': await Request.getCsrf(),\n        'csrf': await Request.getCsrf(),\n        'visit_id': '',\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/http/login.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\nimport 'package:crypto/crypto.dart';\nimport 'package:dio/dio.dart';\nimport 'package:encrypt/encrypt.dart';\nimport 'package:pilipala/http/constants.dart';\nimport 'package:uuid/uuid.dart';\nimport '../models/login/index.dart';\nimport '../utils/login.dart';\nimport 'index.dart';\n\nclass LoginHttp {\n  static Future queryCaptcha() async {\n    var res = await Request().get(Api.getCaptcha);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': CaptchaDataModel.fromJson(res.data['data']),\n      };\n    } else {\n      return {'status': false, 'data': res.message};\n    }\n  }\n\n  // static Future sendSmsCode({\n  //   int? cid,\n  //   required int tel,\n  //   required String token,\n  //   required String challenge,\n  //   required String validate,\n  //   required String seccode,\n  // }) async {\n  //   var res = await Request().post(\n  //     Api.appSmsCode,\n  //     data: {\n  //       'cid': cid,\n  //       'tel': tel,\n  //       \"source\": \"main_web\",\n  //       'token': token,\n  //       'challenge': challenge,\n  //       'validate': validate,\n  //       'seccode': seccode,\n  //     },\n  //     options: Options(\n  //       contentType: Headers.formUrlEncodedContentType,\n  //       // headers: {'user-agent': ApiConstants.userAgent}\n  //     ),\n  //   );\n  //   print(res);\n  // }\n\n  // web端验证码\n  static Future sendWebSmsCode({\n    int? cid,\n    required int tel,\n    required String token,\n    required String challenge,\n    required String validate,\n    required String seccode,\n  }) async {\n    Map data = {\n      'cid': cid,\n      'tel': tel,\n      \"source\": \"main_web\",\n      'token': token,\n      'challenge': challenge,\n      'validate': validate,\n      'seccode': seccode,\n    };\n    FormData formData = FormData.fromMap({...data});\n    var res = await Request().post(\n      Api.webSmsCode,\n      data: formData,\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // web端验证码登录\n  static Future loginInByWebSmsCode({\n    int? cid,\n    required int tel,\n    required int code,\n    required String captchaKey,\n  }) async {\n    // webSmsLogin\n    Map data = {\n      \"cid\": cid,\n      \"tel\": tel,\n      \"code\": code,\n      \"source\": \"main_mini\",\n      \"keep\": 0,\n      \"captcha_key\": captchaKey,\n      \"go_url\": HttpString.baseUrl\n    };\n    FormData formData = FormData.fromMap({...data});\n    var res = await Request().post(\n      Api.webSmsLogin,\n      data: formData,\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // web端密码登录\n  static Future liginInByWebPwd() async {}\n\n  // app端验证码\n  static Future sendAppSmsCode({\n    int? cid,\n    required int tel,\n    required String token,\n    required String challenge,\n    required String validate,\n    required String seccode,\n  }) async {\n    Map<String, dynamic> data = {\n      'cid': cid,\n      'tel': tel,\n      'login_session_id': const Uuid().v4().replaceAll('-', ''),\n      'recaptcha_token': token,\n      'gee_challenge': challenge,\n      'gee_validate': validate,\n      'gee_seccode': seccode,\n      'channel': 'bili',\n      'buvid': buvid(),\n      'local_id': buvid(),\n      // 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,\n      'statistics': {\n        \"appId\": 1,\n        \"platform\": 3,\n        \"version\": \"7.52.0\",\n        \"abtest\": \"\"\n      },\n    };\n    // FormData formData = FormData.fromMap({...data});\n    var res = await Request().post(\n      Api.appSmsCode,\n      data: data,\n    );\n    print(res);\n  }\n\n  static String buvid() {\n    var mac = <String>[];\n    var random = Random();\n\n    for (var i = 0; i < 6; i++) {\n      var min = 0;\n      var max = 0xff;\n      var num = (random.nextInt(max - min + 1) + min).toRadixString(16);\n      mac.add(num);\n    }\n\n    var md5Str = md5.convert(utf8.encode(mac.join(':'))).toString();\n    var md5Arr = md5Str.split('');\n    return 'XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5Str';\n  }\n\n  // 获取盐hash跟PubKey\n  static Future getWebKey() async {\n    var res = await Request().get(Api.getWebKey,\n        data: {'disable_rcmd': 0, 'local_id': LoginUtils.generateBuvid()});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': {}, 'msg': res.data['message']};\n    }\n  }\n\n  // app端密码登录\n  static Future loginInByMobPwd({\n    required String tel,\n    required String password,\n    required String key,\n    required String rhash,\n  }) async {\n    dynamic publicKey = RSAKeyParser().parse(key);\n    String passwordEncryptyed =\n        Encrypter(RSA(publicKey: publicKey)).encrypt(rhash + password).base64;\n    Map<String, dynamic> data = {\n      'username': tel,\n      'password': passwordEncryptyed,\n      'local_id': LoginUtils.generateBuvid(),\n      'disable_rcmd': \"0\",\n    };\n    var res = await Request().post(\n      Api.loginInByPwdApi,\n      data: data,\n    );\n    print(res);\n  }\n\n  // web端密码登录\n  static Future loginInByWebPwd({\n    required int username,\n    required String password,\n    required String token,\n    required String challenge,\n    required String validate,\n    required String seccode,\n  }) async {\n    Map data = {\n      'username': username,\n      'password': password,\n      'keep': 0,\n      'token': token,\n      'challenge': challenge,\n      'validate': validate,\n      'seccode': seccode,\n      'source': 'main-fe-header',\n      \"go_url\": HttpString.baseUrl\n    };\n    FormData formData = FormData.fromMap({...data});\n    var res = await Request().post(\n      Api.loginInByWebPwd,\n      data: formData,\n    );\n    if (res.data['code'] == 0) {\n      if (res.data['data']['status'] == 0) {\n        return {\n          'status': true,\n          'data': res.data['data'],\n        };\n      } else {\n        return {\n          'status': false,\n          'code': 1,\n          'data': res.data['data'],\n          'msg': res.data['data']['message'],\n        };\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // web端登录二维码\n  static Future getWebQrcode() async {\n    var res = await Request().get(Api.qrCodeApi);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // web端二维码轮询登录状态\n  static Future queryWebQrcodeStatus(String qrcodeKey) async {\n    var res = await Request()\n        .get(Api.loginInByQrcode, data: {'qrcode_key': qrcodeKey});\n    if (res.data['data']['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/member.dart",
    "content": "import 'dart:convert';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:hive/hive.dart';\nimport 'package:html/parser.dart';\nimport 'package:pilipala/models/member/article.dart';\nimport 'package:pilipala/models/member/like.dart';\nimport '../common/constants.dart';\nimport '../models/dynamics/result.dart';\nimport '../models/follow/result.dart';\nimport '../models/member/archive.dart';\nimport '../models/member/coin.dart';\nimport '../models/member/info.dart';\nimport '../models/member/seasons.dart';\nimport '../models/member/tags.dart';\nimport '../utils/storage.dart';\nimport '../utils/utils.dart';\nimport '../utils/wbi_sign.dart';\nimport 'index.dart';\n\nclass MemberHttp {\n  static Future memberInfo({\n    int? mid,\n    String token = '',\n  }) async {\n    Map params = await WbiSign().makSign({\n      'mid': mid,\n      'token': token,\n      'platform': 'web',\n      'web_location': 1550101,\n    });\n    var res = await Request().get(\n      Api.memberInfo,\n      data: params,\n      extra: {'ua': 'pc'},\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': MemberInfoModel.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future memberStat({int? mid}) async {\n    var res = await Request().get(Api.userStat, data: {'vmid': mid});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future memberCardInfo({int? mid}) async {\n    var res = await Request()\n        .get(Api.memberCardInfo, data: {'mid': mid, 'photo': true});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future memberArchive({\n    int? mid,\n    int ps = 30,\n    int tid = 0,\n    int? pn,\n    String? keyword,\n    String order = 'pubdate',\n    bool orderAvoided = true,\n  }) async {\n    String dmImgStr = Utils.base64EncodeRandomString(16, 64);\n    String dmCoverImgStr = Utils.base64EncodeRandomString(32, 128);\n    Map params = await WbiSign().makSign({\n      'mid': mid,\n      'ps': ps,\n      'tid': tid,\n      'pn': pn,\n      'keyword': keyword ?? '',\n      'order': order,\n      'platform': 'web',\n      'web_location': 1550101,\n      'order_avoided': orderAvoided,\n      'dm_img_list': '[]',\n      'dm_img_str': dmImgStr.substring(0, dmImgStr.length - 2),\n      'dm_cover_img_str': dmCoverImgStr.substring(0, dmCoverImgStr.length - 2),\n      'dm_img_inter': '{\"ds\":[],\"wh\":[0,0,0],\"of\":[0,0,0]}',\n      ...order == 'charge'\n          ? {\n              'order': 'pubdate',\n              'special_type': 'charging',\n            }\n          : {}\n    });\n\n    var res = await Request().get(\n      Api.memberArchive,\n      data: params,\n      extra: {'ua': 'pc'},\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': MemberArchiveDataModel.fromJson(res.data['data'])\n      };\n    } else {\n      Map errMap = {\n        -352: '风控校验失败，请检查登录状态',\n      };\n      return {\n        'status': false,\n        'data': [],\n        'msg': errMap[res.data['code']] ?? res.data['message'],\n      };\n    }\n  }\n\n  // 用户动态\n  static Future memberDynamic({String? offset, int? mid}) async {\n    var res = await Request().get(Api.memberDynamic, data: {\n      'offset': offset ?? '',\n      'host_mid': mid,\n      'timezone_offset': '-480',\n      'features': 'itemOpusStyle',\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': DynamicsDataModel.fromJson(res.data['data']),\n      };\n    } else {\n      Map errMap = {\n        -352: '风控校验失败，请检查登录状态',\n      };\n      return {\n        'status': false,\n        'data': [],\n        'msg': errMap[res.data['code']] ?? res.data['message'],\n      };\n    }\n  }\n\n  // 搜索用户动态\n  static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {\n    var res = await Request().get(Api.memberDynamic, data: {\n      'keyword': '海拔',\n      'mid': mid,\n      'pn': pn,\n      'ps': ps,\n      'platform': 'web'\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': DynamicsDataModel.fromJson(res.data['data']),\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 查询分组\n  static Future followUpTags() async {\n    var res = await Request().get(Api.followUpTag);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data']\n            .map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))\n            .toList()\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 设置分组\n  static Future addUsers(int? fids, String? tagids) async {\n    var res = await Request().post(\n      Api.addUsers,\n      data: {\n        'fids': fids,\n        'tagids': tagids ?? '0',\n        'csrf': await Request.getCsrf(),\n      },\n      queryParameters: {'cross_domain': true},\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': [], 'msg': '操作成功'};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 获取某分组下的up\n  static Future followUpGroup(\n    int? mid,\n    int? tagid,\n    int? pn,\n    int? ps,\n  ) async {\n    var res = await Request().get(Api.followUpGroup, data: {\n      'mid': mid,\n      'tagid': tagid,\n      'pn': pn,\n      'ps': ps,\n    });\n    if (res.data['code'] == 0) {\n      // FollowItemModel\n      return {\n        'status': true,\n        'data': res.data['data']\n            .map<FollowItemModel>((e) => FollowItemModel.fromJson(e))\n            .toList()\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 获取up置顶\n  static Future getTopVideo(String? vmid) async {\n    var res = await Request().get(Api.getTopVideoApi);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data']\n            .map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))\n            .toList()\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 获取uo专栏\n  static Future getMemberSeasons(int? mid, int? pn, int? ps) async {\n    var res = await Request().get(Api.getMemberSeasonsApi, data: {\n      'mid': mid,\n      'page_num': pn,\n      'page_size': ps,\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 最近投币\n  static Future getRecentCoinVideo({required int mid}) async {\n    Map params = await WbiSign().makSign({\n      'mid': mid,\n      'gaia_source': 'main_web',\n      'web_location': 333.999,\n    });\n    var res = await Request().get(\n      Api.getRecentCoinVideoApi,\n      data: {\n        'vmid': mid,\n        'gaia_source': 'main_web',\n        'web_location': 333.999,\n        'w_rid': params['w_rid'],\n        'wts': params['wts'],\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data']\n            .map<MemberCoinsDataModel>((e) => MemberCoinsDataModel.fromJson(e))\n            .toList(),\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 最近点赞\n  static Future getRecentLikeVideo({required int mid}) async {\n    Map params = await WbiSign().makSign({\n      'mid': mid,\n      'gaia_source': 'main_web',\n      'web_location': 333.999,\n    });\n    var res = await Request().get(\n      Api.getRecentLikeVideoApi,\n      data: {\n        'vmid': mid,\n        'gaia_source': 'main_web',\n        'web_location': 333.999,\n        'w_rid': params['w_rid'],\n        'wts': params['wts'],\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data']['list']\n            .map<MemberLikeDataModel>((e) => MemberLikeDataModel.fromJson(e))\n            .toList(),\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 查看某个专栏\n  static Future getSeasonDetail({\n    required int mid,\n    required int seasonId,\n    bool sortReverse = false,\n    required int pn,\n    required int ps,\n  }) async {\n    var res = await Request().get(\n      Api.getSeasonDetailApi,\n      data: {\n        'mid': mid,\n        'season_id': seasonId,\n        'sort_reverse': sortReverse,\n        'page_num': pn,\n        'page_size': ps,\n      },\n    );\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': MemberSeasonsList.fromJson(res.data['data'])\n        };\n      } catch (err) {\n        print(err);\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 获取TV authCode\n  static Future getTVCode() async {\n    SmartDialog.showLoading();\n    var params = {\n      'appkey': Constants.appKey,\n      'local_id': '0',\n      'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),\n    };\n    String sign = Utils.appSign(\n      params,\n      Constants.appKey,\n      Constants.appSec,\n    );\n    var res = await Request()\n        .post(Api.getTVCode, queryParameters: {...params, 'sign': sign});\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data']['auth_code'],\n        'msg': '操作成功'\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 获取access_key\n  static Future cookieToKey() async {\n    var authCodeRes = await getTVCode();\n    if (authCodeRes['status']) {\n      var res = await Request().post(\n        Api.cookieToKey,\n        data: {\n          'auth_code': authCodeRes['data'],\n          'build': 708200,\n          'csrf': await Request.getCsrf(),\n        },\n      );\n      await Future.delayed(const Duration(milliseconds: 300));\n      await qrcodePoll(authCodeRes['data']);\n      if (res.data['code'] == 0) {\n        return {'status': true, 'data': [], 'msg': '操作成功'};\n      } else {\n        return {\n          'status': false,\n          'data': [],\n          'msg': res.data['message'],\n        };\n      }\n    }\n  }\n\n  static Future qrcodePoll(authCode) async {\n    var params = {\n      'appkey': Constants.appKey,\n      'auth_code': authCode.toString(),\n      'local_id': '0',\n      'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),\n    };\n    String sign = Utils.appSign(\n      params,\n      Constants.appKey,\n      Constants.appSec,\n    );\n    var res = await Request()\n        .post(Api.qrcodePoll, queryParameters: {...params, 'sign': sign});\n    SmartDialog.dismiss();\n    if (res.data['code'] == 0) {\n      String accessKey = res.data['data']['access_token'];\n      Box localCache = GStrorage.localCache;\n      Box userInfoCache = GStrorage.userInfo;\n      var userInfo = userInfoCache.get('userInfoCache');\n      localCache.put(\n          LocalCacheKey.accessKey, {'mid': userInfo.mid, 'value': accessKey});\n      return {'status': true, 'data': [], 'msg': '操作成功'};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 获取up播放数、点赞数\n  static Future memberView({required int mid}) async {\n    var res = await Request().get(Api.getMemberViewApi, data: {'mid': mid});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 搜索follow\n  static Future getfollowSearch({\n    required int mid,\n    required int ps,\n    required int pn,\n    required String name,\n  }) async {\n    Map<String, dynamic> data = {\n      'vmid': mid,\n      'pn': pn,\n      'ps': ps,\n      'order': 'desc',\n      'order_type': 'attention',\n      'gaia_source': 'main_web',\n      'name': name,\n      'web_location': 333.999,\n    };\n    Map params = await WbiSign().makSign(data);\n    var res = await Request().get(Api.followSearch, data: {\n      ...data,\n      'w_rid': params['w_rid'],\n      'wts': params['wts'],\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': FollowDataModel.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future getSeriesDetail({\n    required int mid,\n    required int currentMid,\n    required int seriesId,\n    required int pn,\n  }) async {\n    var res = await Request().get(\n      Api.getSeriesDetailApi,\n      data: {\n        'mid': mid,\n        'series_id': seriesId,\n        'only_normal': true,\n        'sort': 'desc',\n        'pn': pn,\n        'ps': 30,\n        'current_mid': currentMid,\n      },\n    );\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': MemberSeasonsDataModel.fromJson(res.data['data'])\n        };\n      } catch (err) {\n        print(err);\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future getWWebid({required int mid}) async {\n    var res = await Request().get('https://space.bilibili.com/$mid/article');\n    String? headContent = parse(res.data).head?.outerHtml;\n    final regex = RegExp(\n        r'<script id=\"__RENDER_DATA__\" type=\"application/json\">(.*?)</script>');\n    if (headContent != null) {\n      final match = regex.firstMatch(headContent);\n      if (match != null && match.groupCount >= 1) {\n        final content = match.group(1);\n        String decodedString = Uri.decodeComponent(content!);\n        Map<String, dynamic> map = jsonDecode(decodedString);\n        return {'status': true, 'data': map['access_id']};\n      } else {\n        return {'status': false, 'data': '请检查登录状态'};\n      }\n    }\n    return {'status': false, 'data': '请检查登录状态'};\n  }\n\n  // 获取用户专栏\n  static Future getMemberArticle({\n    required int mid,\n    required int pn,\n    required String wWebid,\n    String? offset,\n  }) async {\n    Map params = await WbiSign().makSign({\n      'host_mid': mid,\n      'page': pn,\n      'offset': offset,\n      'web_location': 333.999,\n      'w_webid': wWebid,\n    });\n    var res = await Request().get(Api.opusList, data: {\n      'host_mid': mid,\n      'page': pn,\n      'offset': offset,\n      'web_location': 333.999,\n      'w_webid': wWebid,\n      'w_rid': params['w_rid'],\n      'wts': params['wts'],\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': MemberArticleDataModel.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'] ?? '请求异常',\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/msg.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\nimport 'package:pilipala/models/msg/like.dart';\nimport 'package:pilipala/models/msg/reply.dart';\nimport 'package:pilipala/models/msg/system.dart';\nimport '../models/msg/account.dart';\nimport '../models/msg/session.dart';\nimport '../utils/wbi_sign.dart';\nimport 'api.dart';\nimport 'init.dart';\n\nclass MsgHttp {\n  // 会话列表\n  static Future sessionList({int? endTs}) async {\n    Map<String, dynamic> params = {\n      'session_type': 1,\n      'group_fold': 1,\n      'unfollow_fold': 0,\n      'sort_rule': 2,\n      'build': 0,\n      'mobi_app': 'web',\n    };\n    if (endTs != null) {\n      params['end_ts'] = endTs;\n    }\n\n    Map signParams = await WbiSign().makSign(params);\n    var res = await Request().get(Api.sessionList, data: signParams);\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': SessionDataModel.fromJson(res.data['data']),\n        };\n      } catch (err) {\n        return {\n          'status': false,\n          'data': [],\n          'msg': err.toString(),\n        };\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future accountList(uids) async {\n    var res = await Request().get(Api.sessionAccountList, data: {\n      'uids': uids,\n      'build': 0,\n      'mobi_app': 'web',\n    });\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': res.data['data']\n              .map<AccountListModel>((e) => AccountListModel.fromJson(e))\n              .toList(),\n        };\n      } catch (err) {\n        print('err🔟: $err');\n      }\n    } else {\n      return {\n        'status': false,\n        'date': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future sessionMsg({\n    int? talkerId,\n  }) async {\n    Map params = await WbiSign().makSign({\n      'talker_id': talkerId,\n      'session_type': 1,\n      'size': 20,\n      'sender_device_id': 1,\n      'build': 0,\n      'mobi_app': 'web',\n    });\n    var res = await Request().get(Api.sessionMsg, data: params);\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': SessionMsgDataModel.fromJson(res.data['data']),\n        };\n      } catch (err) {\n        print(err);\n      }\n    } else {\n      return {\n        'status': false,\n        'date': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 消息标记已读\n  static Future ackSessionMsg({\n    int? talkerId,\n    int? ackSeqno,\n  }) async {\n    String csrf = await Request.getCsrf();\n    Map params = await WbiSign().makSign({\n      'talker_id': talkerId,\n      'session_type': 1,\n      'ack_seqno': ackSeqno,\n      'build': 0,\n      'mobi_app': 'web',\n      'csrf_token': csrf,\n      'csrf': csrf\n    });\n    var res = await Request().get(Api.ackSessionMsg, data: params);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {'status': false, 'date': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 发送私信\n  static Future sendMsg({\n    required int senderUid,\n    required int receiverId,\n    int? receiverType,\n    int? msgType,\n    dynamic content,\n  }) async {\n    String csrf = await Request.getCsrf();\n    var res = await Request().post(\n      Api.sendMsg,\n      data: {\n        'msg[sender_uid]': senderUid,\n        'msg[receiver_id]': receiverId,\n        'msg[receiver_type]': 1,\n        'msg[msg_type]': 1,\n        'msg[msg_status]': 0,\n        'msg[content]': jsonEncode(content),\n        'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000,\n        'msg[new_face_version]': 1,\n        'msg[dev_id]': getDevId(),\n        'from_firework': 0,\n        'build': 0,\n        'mobi_app': 'web',\n        'csrf_token': csrf,\n        'csrf': csrf,\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {'status': false, 'date': [], 'msg': res.data['message']};\n    }\n  }\n\n  static String getDevId() {\n    final List<String> b = [\n      '0',\n      '1',\n      '2',\n      '3',\n      '4',\n      '5',\n      '6',\n      '7',\n      '8',\n      '9',\n      'A',\n      'B',\n      'C',\n      'D',\n      'E',\n      'F'\n    ];\n    final List<String> s = \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".split('');\n    for (int i = 0; i < s.length; i++) {\n      if ('-' == s[i] || '4' == s[i]) {\n        continue;\n      }\n      final int randomInt = Random().nextInt(16);\n      if ('x' == s[i]) {\n        s[i] = b[randomInt];\n      } else {\n        s[i] = b[3 & randomInt | 8];\n      }\n    }\n    return s.join();\n  }\n\n  static Future removeSession({\n    int? talkerId,\n  }) async {\n    String csrf = await Request.getCsrf();\n    Map params = await WbiSign().makSign({\n      'talker_id': talkerId,\n      'session_type': 1,\n      'build': 0,\n      'mobi_app': 'web',\n      'csrf_token': csrf,\n      'csrf': csrf\n    });\n    var res = await Request().get(Api.removeSession, data: params);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {'status': false, 'date': [], 'msg': res.data['message']};\n    }\n  }\n\n  static Future unread() async {\n    var res = await Request().get(Api.unread);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {'status': false, 'date': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 回复我的\n  static Future messageReply({\n    int? id,\n    int? replyTime,\n  }) async {\n    var params = {\n      if (id != null) 'id': id,\n      if (replyTime != null) 'reply_time': replyTime,\n    };\n    var res = await Request().get(Api.messageReplyAPi, data: params);\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': MessageReplyModel.fromJson(res.data['data']),\n        };\n      } catch (err) {\n        return {'status': false, 'date': [], 'msg': err.toString()};\n      }\n    } else {\n      return {'status': false, 'date': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 收到的赞\n  static Future messageLike({\n    int? id,\n    int? likeTime,\n  }) async {\n    var params = {\n      if (id != null) 'id': id,\n      if (likeTime != null) 'like_time': likeTime,\n    };\n    var res = await Request().get(Api.messageLikeAPi, data: params);\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': MessageLikeModel.fromJson(res.data['data']),\n        };\n      } catch (err) {\n        return {'status': false, 'date': [], 'msg': err.toString()};\n      }\n    } else {\n      return {'status': false, 'date': [], 'msg': res.data['message']};\n    }\n  }\n\n  static Future messageSystem() async {\n    var res = await Request().get(Api.messageSystemAPi, data: {\n      'csrf': await Request.getCsrf(),\n      'page_size': 20,\n      'build': 0,\n      'mobi_app': 'web',\n    });\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': res.data['data']['system_notify_list']\n              .map<MessageSystemModel>((e) => MessageSystemModel.fromJson(e))\n              .toList(),\n        };\n      } catch (err) {\n        return {'status': false, 'date': [], 'msg': err.toString()};\n      }\n    } else {\n      return {'status': false, 'date': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 系统消息标记已读\n  static Future systemMarkRead(int cursor) async {\n    String csrf = await Request.getCsrf();\n    var res = await Request().get(Api.systemMarkRead, data: {\n      'csrf': csrf,\n      'cursor': cursor,\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n      };\n    } else {\n      return {\n        'status': false,\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future messageSystemAccount() async {\n    var res = await Request().get(Api.userMessageSystemAPi, data: {\n      'csrf': await Request.getCsrf(),\n      'page_size': 20,\n      'build': 0,\n      'mobi_app': 'web',\n    });\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': res.data['data']['system_notify_list']\n              .map<MessageSystemModel>((e) => MessageSystemModel.fromJson(e))\n              .toList(),\n        };\n      } catch (err) {\n        return {'status': false, 'date': [], 'msg': err.toString()};\n      }\n    } else {\n      return {'status': false, 'date': [], 'msg': res.data['message']};\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/read.dart",
    "content": "import 'dart:convert';\nimport 'package:html/parser.dart';\nimport 'package:pilipala/models/read/opus.dart';\nimport 'package:pilipala/models/read/read.dart';\nimport 'package:pilipala/utils/wbi_sign.dart';\nimport 'index.dart';\n\nclass ReadHttp {\n  static List<String> extractScriptContents(String htmlContent) {\n    RegExp scriptRegExp = RegExp(r'<script>([\\s\\S]*?)<\\/script>');\n    Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);\n    List<String> scriptContents = [];\n    for (Match match in matches) {\n      String scriptContent = match.group(1)!;\n      scriptContents.add(scriptContent);\n    }\n    return scriptContents;\n  }\n\n  // 解析专栏 opus格式\n  static Future parseArticleOpus({required String id}) async {\n    var res = await Request().get('https://www.bilibili.com/opus/$id', extra: {\n      'ua': 'pc',\n    });\n    String? headContent = parse(res.data).head?.outerHtml;\n    var document = parse(headContent);\n    var linkTags = document.getElementsByTagName('link');\n    bool isCv = false;\n    String cvId = '';\n    for (var linkTag in linkTags) {\n      var attributes = linkTag.attributes;\n      if (attributes.containsKey('rel') &&\n          attributes['rel'] == 'canonical' &&\n          attributes.containsKey('data-vue-meta') &&\n          attributes['data-vue-meta'] == 'true') {\n        final String cvHref = linkTag.attributes['href']!;\n        RegExp regex = RegExp(r'cv(\\d+)');\n        RegExpMatch? match = regex.firstMatch(cvHref);\n        if (match != null) {\n          cvId = match.group(1)!;\n        } else {\n          print('No match found.');\n        }\n        isCv = true;\n        break;\n      }\n    }\n    String scriptContent =\n        extractScriptContents(parse(res.data).body!.outerHtml)[0];\n    int startIndex = scriptContent.indexOf('{');\n    int endIndex = scriptContent.lastIndexOf('};');\n    String jsonContent = scriptContent.substring(startIndex, endIndex + 1);\n    // 解析JSON字符串为Map\n    Map<String, dynamic> jsonData = json.decode(jsonContent);\n    return {\n      'status': true,\n      'data': OpusDataModel.fromJson(jsonData),\n      'isCv': isCv,\n      'cvId': cvId,\n    };\n  }\n\n  // 解析专栏 cv格式\n  static Future parseArticleCv({required String id}) async {\n    var res = await Request().get(\n      'https://www.bilibili.com/read/cv$id',\n      extra: {'ua': 'pc'},\n    );\n    String scriptContent =\n        extractScriptContents(parse(res.data).body!.outerHtml)[0];\n    int startIndex = scriptContent.indexOf('{');\n    int endIndex = scriptContent.lastIndexOf('};');\n    String jsonContent = scriptContent.substring(startIndex, endIndex + 1);\n    // 解析JSON字符串为Map\n    Map<String, dynamic> jsonData = json.decode(jsonContent);\n    return {\n      'status': true,\n      'data': ReadDataModel.fromJson(jsonData),\n    };\n  }\n\n  //\n  static Future getViewInfo({required String id}) async {\n    Map params = await WbiSign().makSign({\n      'id': id,\n      'mobi_app': 'pc',\n      'from': 'web',\n      'gaia_source': 'main_web',\n      'web_location': 333.976,\n    });\n    var res = await Request().get(\n      Api.getViewInfo,\n      data: {\n        'id': id,\n        'mobi_app': 'pc',\n        'from': 'web',\n        'gaia_source': 'main_web',\n        'web_location': 333.976,\n        'w_rid': params['w_rid'],\n        'wts': params['wts'],\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data'],\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/reply.dart",
    "content": "import '../models/video/reply/data.dart';\nimport '../models/video/reply/emote.dart';\nimport 'api.dart';\nimport 'init.dart';\n\nclass ReplyHttp {\n  static Future replyList({\n    required int oid,\n    required int pageNum,\n    required int type,\n    int? ps,\n    int sort = 1,\n  }) async {\n    var res = await Request().get(Api.replyList, data: {\n      'oid': oid,\n      'pn': pageNum,\n      'type': type,\n      'sort': sort,\n      'ps': ps ?? 20\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': ReplyData.fromJson(res.data['data']),\n        'code': 200,\n      };\n    } else {\n      return {\n        'status': false,\n        'date': [],\n        'code': res.data['code'],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future replyReplyList({\n    required int oid,\n    required String root,\n    required int pageNum,\n    required int type,\n    int sort = 1,\n  }) async {\n    var res = await Request().get(Api.replyReplyList, data: {\n      'oid': oid,\n      'root': root,\n      'pn': pageNum,\n      'type': type,\n      'sort': 1,\n      'csrf': await Request.getCsrf(),\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': ReplyData.fromJson(res.data['data']),\n      };\n    } else {\n      Map errMap = {\n        -400: '请求错误',\n        -404: '无此项',\n        12002: '评论区已关闭',\n        12009: '评论主体的type不合法',\n      };\n      return {\n        'status': false,\n        'date': [],\n        'msg': errMap[res.data['code']] ?? '请求异常',\n      };\n    }\n  }\n\n  // 评论点赞\n  static Future likeReply({\n    required int type,\n    required int oid,\n    required int rpid,\n    required int action,\n  }) async {\n    var res = await Request().post(\n      Api.likeReply,\n      data: {\n        'type': type,\n        'oid': oid,\n        'rpid': rpid,\n        'action': action,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {\n        'status': false,\n        'date': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future getEmoteList({String? business}) async {\n    var res = await Request().get(Api.emojiList, data: {\n      'business': business ?? 'reply',\n      'web_location': '333.1245',\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': EmoteModelData.fromJson(res.data['data']),\n      };\n    } else {\n      return {\n        'status': false,\n        'date': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future replyDel({\n    required int type, //replyType\n    required int oid,\n    required int rpid,\n  }) async {\n    var res = await Request().post(\n      Api.replyDel,\n      queryParameters: {\n        'type': type, //type.index\n        'oid': oid,\n        'rpid': rpid,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'msg': '删除成功'};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n}\n"
  },
  {
    "path": "lib/http/search.dart",
    "content": "import 'dart:convert';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/search/all.dart';\nimport 'package:pilipala/utils/wbi_sign.dart';\nimport '../models/bangumi/info.dart';\nimport '../models/common/search_type.dart';\nimport '../models/search/hot.dart';\nimport '../models/search/result.dart';\nimport '../models/search/suggest.dart';\nimport '../utils/storage.dart';\nimport 'index.dart';\n\nclass SearchHttp {\n  static Box setting = GStrorage.setting;\n  static Future hotSearchList() async {\n    var res = await Request().get(Api.hotSearchList);\n    if (res.data is String) {\n      Map<String, dynamic> resultMap = json.decode(res.data);\n      if (resultMap['code'] == 0) {\n        return {\n          'status': true,\n          'data': HotSearchModel.fromJson(resultMap),\n        };\n      }\n    } else if (res.data is Map<String, dynamic> && res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': HotSearchModel.fromJson(res.data),\n      };\n    }\n\n    return {\n      'status': false,\n      'data': [],\n      'msg': '请求错误 🙅',\n    };\n  }\n\n  // 获取搜索建议\n  static Future searchSuggest({required term}) async {\n    var res = await Request().get(Api.searchSuggest,\n        data: {'term': term, 'main_ver': 'v1', 'highlight': term});\n    if (res.data is String) {\n      Map<String, dynamic> resultMap = json.decode(res.data);\n      if (resultMap['code'] == 0) {\n        if (resultMap['result'] is Map) {\n          resultMap['result']['term'] = term;\n        }\n        return {\n          'status': true,\n          'data': resultMap['result'] is Map\n              ? SearchSuggestModel.fromJson(resultMap['result'])\n              : [],\n        };\n      } else {\n        return {\n          'status': false,\n          'data': [],\n          'msg': '请求错误 🙅',\n        };\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': '请求错误 🙅',\n      };\n    }\n  }\n\n  // 分类搜索\n  static Future searchByType({\n    required SearchType searchType,\n    required String keyword,\n    required page,\n    String? order,\n    int? duration,\n    int? tids,\n  }) async {\n    var reqData = {\n      'search_type': searchType.type,\n      'keyword': keyword,\n      // 'order_sort': 0,\n      // 'user_type': 0,\n      'page': page,\n      if (order != null) 'order': order,\n      if (duration != null) 'duration': duration,\n      if (tids != null && tids != -1) 'tids': tids,\n    };\n    var res = await Request().get(Api.searchByType, data: reqData);\n    if (res.data['code'] == 0) {\n      if (res.data['data']['numPages'] == 0) {\n        // 我想返回数据，使得可以通过data.list 取值，结果为[]\n        return {'status': true, 'data': Data()};\n      }\n      Object data;\n      try {\n        switch (searchType) {\n          case SearchType.video:\n            List<int> blackMidsList =\n                setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);\n            for (var i in res.data['data']['result']) {\n              // 屏蔽推广和拉黑用户\n              i['available'] = !blackMidsList.contains(i['mid']);\n            }\n            data = SearchVideoModel.fromJson(res.data['data']);\n            break;\n          case SearchType.live_room:\n            data = SearchLiveModel.fromJson(res.data['data']);\n            break;\n          case SearchType.bili_user:\n            data = SearchUserModel.fromJson(res.data['data']);\n            break;\n          case SearchType.media_bangumi:\n            data = SearchMBangumiModel.fromJson(res.data['data']);\n            break;\n          case SearchType.article:\n            data = SearchArticleModel.fromJson(res.data['data']);\n            break;\n        }\n        return {\n          'status': true,\n          'data': data,\n        };\n      } catch (err) {\n        print(err);\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  static Future<int> ab2c({int? aid, String? bvid}) async {\n    Map<String, dynamic> data = {};\n    if (aid != null) {\n      data['aid'] = aid;\n    } else if (bvid != null) {\n      data['bvid'] = bvid;\n    }\n    final dynamic res =\n        await Request().get(Api.ab2c, data: <String, dynamic>{...data});\n    if (res.data['code'] == 0) {\n      return res.data['data'].first['cid'];\n    } else {\n      return -1;\n    }\n  }\n\n  static Future<Map<String, dynamic>> bangumiInfo(\n      {int? seasonId, int? epId}) async {\n    final Map<String, dynamic> data = {};\n    if (seasonId != null) {\n      data['season_id'] = seasonId;\n    } else if (epId != null) {\n      data['ep_id'] = epId;\n    }\n    final dynamic res =\n        await Request().get(Api.bangumiInfo, data: <String, dynamic>{...data});\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': BangumiInfoModel.fromJson(res.data['result']),\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': '请求错误 🙅',\n      };\n    }\n  }\n\n  static Future<Map<String, dynamic>> ab2cWithPic(\n      {int? aid, String? bvid}) async {\n    Map<String, dynamic> data = {};\n    if (aid != null) {\n      data['aid'] = aid;\n    } else if (bvid != null) {\n      data['bvid'] = bvid;\n    }\n    final dynamic res =\n        await Request().get(Api.ab2c, data: <String, dynamic>{...data});\n    return {\n      'cid': res.data['data'].first['cid'],\n      'pic': res.data['data'].first['first_frame'],\n    };\n  }\n\n  static Future<Map<String, dynamic>> searchCount(\n      {required String keyword}) async {\n    Map<String, dynamic> data = {\n      'keyword': keyword,\n      'web_location': 333.999,\n    };\n    Map params = await WbiSign().makSign(data);\n    final dynamic res = await Request().get(Api.searchCount, data: params);\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': SearchAllModel.fromJson(res.data['data']),\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': '请求错误 🙅',\n      };\n    }\n  }\n}\n\nclass Data {\n  List<dynamic> list;\n\n  Data({this.list = const []});\n}\n"
  },
  {
    "path": "lib/http/user.dart",
    "content": "import 'dart:convert';\nimport 'dart:developer';\n\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:html/parser.dart';\nimport 'package:pilipala/models/video/later.dart';\nimport '../common/constants.dart';\nimport '../models/model_hot_video_item.dart';\nimport '../models/user/fav_detail.dart';\nimport '../models/user/fav_folder.dart';\nimport '../models/user/history.dart';\nimport '../models/user/info.dart';\nimport '../models/user/stat.dart';\nimport '../models/user/sub_detail.dart';\nimport '../models/user/sub_folder.dart';\nimport 'api.dart';\nimport 'init.dart';\n\nclass UserHttp {\n  static Future<dynamic> userStat({required int mid}) async {\n    var res = await Request().get(Api.userStat, data: {'vmid': mid});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false};\n    }\n  }\n\n  static Future<dynamic> userInfo() async {\n    var res = await Request().get(Api.userInfo);\n    if (res.data['code'] == 0) {\n      UserInfoData data = UserInfoData.fromJson(res.data['data']);\n      return {'status': true, 'data': data};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  static Future<dynamic> userStatOwner() async {\n    var res = await Request().get(Api.userStatOwner);\n    if (res.data['code'] == 0) {\n      UserStat data = UserStat.fromJson(res.data['data']);\n      return {'status': true, 'data': data};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 收藏夹\n  static Future<dynamic> userfavFolder({\n    required int pn,\n    required int ps,\n    required int mid,\n  }) async {\n    var res = await Request().get(Api.userFavFolder, data: {\n      'pn': pn,\n      'ps': ps,\n      'up_mid': mid,\n    });\n    if (res.data['code'] == 0) {\n      late FavFolderData data;\n      if (res.data['data'] != null) {\n        data = FavFolderData.fromJson(res.data['data']);\n        return {'status': true, 'data': data};\n      } else {\n        return {'status': false, 'msg': '收藏夹为空'};\n      }\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n        'code': res.data['code'],\n      };\n    }\n  }\n\n  static Future<dynamic> userFavFolderDetail(\n      {required int mediaId,\n      required int pn,\n      required int ps,\n      String keyword = '',\n      String order = 'mtime',\n      int type = 0}) async {\n    var res = await Request().get(Api.userFavFolderDetail, data: {\n      'media_id': mediaId,\n      'pn': pn,\n      'ps': ps,\n      'keyword': keyword,\n      'order': order,\n      'type': type,\n      'tid': 0,\n      'platform': 'web'\n    });\n    if (res.data['code'] == 0) {\n      FavDetailData data = FavDetailData.fromJson(res.data['data']);\n      return {'status': true, 'data': data};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 稍后再看\n  static Future<dynamic> seeYouLater() async {\n    var res = await Request().get(Api.seeYouLater);\n    if (res.data['code'] == 0) {\n      if (res.data['data']['count'] == 0) {\n        return {\n          'status': true,\n          'data': {'list': [], 'count': 0}\n        };\n      }\n      List<HotVideoItemModel> list = [];\n      for (var i in res.data['data']['list']) {\n        list.add(HotVideoItemModel.fromJson(i));\n      }\n      return {\n        'status': true,\n        'data': {'list': list, 'count': res.data['data']['count']}\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n        'code': res.data['code'],\n      };\n    }\n  }\n\n  // 观看历史\n  static Future historyList(int? max, int? viewAt) async {\n    var res = await Request().get(Api.historyList, data: {\n      'type': 'all',\n      'ps': 20,\n      'max': max ?? 0,\n      'view_at': viewAt ?? 0,\n    });\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n        'code': res.data['code'],\n      };\n    }\n  }\n\n  // 暂停观看历史\n  static Future pauseHistory(bool switchStatus) async {\n    // 暂停switchStatus传true 否则false\n    var res = await Request().post(\n      Api.pauseHistory,\n      data: {\n        'switch': switchStatus,\n        'jsonp': 'jsonp',\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    return res;\n  }\n\n  // 观看历史暂停状态\n  static Future historyStatus() async {\n    var res = await Request().get(Api.historyStatus);\n    return res;\n  }\n\n  // 清空历史记录\n  static Future clearHistory() async {\n    var res = await Request().post(\n      Api.clearHistory,\n      data: {\n        'jsonp': 'jsonp',\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    return res;\n  }\n\n  // 稍后再看\n  static Future toViewLater({String? bvid, dynamic aid}) async {\n    var data = {'csrf': await Request.getCsrf()};\n    if (bvid != null) {\n      data['bvid'] = bvid;\n    } else if (aid != null) {\n      data['aid'] = aid;\n    }\n    var res = await Request().post(\n      Api.toViewLater,\n      data: data,\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'msg': 'yeah！稍后再看'};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  // 移除已观看\n  static Future toViewDel({int? aid}) async {\n    final Map<String, dynamic> params = {\n      'jsonp': 'jsonp',\n      'csrf': await Request.getCsrf(),\n    };\n\n    params[aid != null ? 'aid' : 'viewed'] = aid ?? true;\n    var res = await Request().post(\n      Api.toViewDel,\n      data: params,\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'msg': 'yeah！成功移除'};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  // 获取用户凭证 失效\n  static Future thirdLogin() async {\n    var res = await Request().get(\n      'https://passport.bilibili.com/login/app/third',\n      data: {\n        'appkey': Constants.appKey,\n        'api': Constants.thirdApi,\n        'sign': Constants.thirdSign,\n      },\n    );\n    try {\n      if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {\n        Request().get(res.data['data']['confirm_uri']);\n      }\n    } catch (err) {\n      SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error);\n    }\n  }\n\n  // 清空稍后再看\n  static Future toViewClear() async {\n    var res = await Request().post(\n      Api.toViewClear,\n      data: {\n        'jsonp': 'jsonp',\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'msg': '操作完成'};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  // 删除历史记录\n  static Future delHistory(kid) async {\n    var res = await Request().post(\n      Api.delHistory,\n      data: {\n        'kid': kid,\n        'jsonp': 'jsonp',\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'msg': '已删除'};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  static Future hasFollow(int mid) async {\n    var res = await Request().get(\n      Api.hasFollow,\n      data: {\n        'fid': mid,\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n  // // 相互关系查询\n  // static Future relationSearch(int mid) async {\n  //   Map params = await WbiSign().makSign({\n  //     'mid': mid,\n  //     'token': '',\n  //     'platform': 'web',\n  //     'web_location': 1550101,\n  //   });\n  //   var res = await Request().get(\n  //     Api.relationSearch,\n  //     data: {\n  //       'mid': mid,\n  //       'w_rid': params['w_rid'],\n  //       'wts': params['wts'],\n  //     },\n  //   );\n  //   if (res.data['code'] == 0) {\n  //     // relation 主动状态\n  //     // 被动状态\n  //     return {'status': true, 'data': res.data['data']};\n  //   } else {\n  //     return {'status': false, 'msg': res.data['message']};\n  //   }\n  // }\n\n  // 搜索历史记录\n  static Future searchHistory(\n      {required int pn, required String keyword}) async {\n    var res = await Request().get(\n      Api.searchHistory,\n      data: {\n        'pn': pn,\n        'keyword': keyword,\n        'business': 'all',\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  // 我的订阅\n  static Future userSubFolder({\n    required int mid,\n    required int pn,\n    required int ps,\n  }) async {\n    var res = await Request().get(Api.userSubFolder, data: {\n      'up_mid': mid,\n      'ps': ps,\n      'pn': pn,\n      'platform': 'web',\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': SubFolderModelData.fromJson(res.data['data'])\n      };\n    } else {\n      return {\n        'status': false,\n        'data': [],\n        'msg': res.data['message'],\n        'code': res.data['code'],\n      };\n    }\n  }\n\n  static Future userSeasonList({\n    required int seasonId,\n    required int pn,\n    required int ps,\n  }) async {\n    var res = await Request().get(Api.userSeasonList, data: {\n      'season_id': seasonId,\n      'ps': ps,\n      'pn': pn,\n    });\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': SubDetailModelData.fromJson(res.data['data'])\n      };\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  static Future userResourceList({\n    required int seasonId,\n    required int pn,\n    required int ps,\n  }) async {\n    var res = await Request().get(Api.userResourceList, data: {\n      'media_id': seasonId,\n      'ps': ps,\n      'pn': pn,\n      'keyword': '',\n      'order': 'mtime',\n      'type': 0,\n      'tid': 0,\n      'platform': 'web',\n    });\n    if (res.data['code'] == 0) {\n      try {\n        return {\n          'status': true,\n          'data': SubDetailModelData.fromJson(res.data['data'])\n        };\n      } catch (err) {\n        return {'status': false, 'msg': err};\n      }\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  // 取消订阅\n  static Future cancelSub({required int seasonId}) async {\n    var res = await Request().post(\n      Api.cancelSub,\n      data: {\n        'platform': 'web',\n        'season_id': seasonId,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  // 删除文件夹\n  static Future delFavFolder({required int mediaIds}) async {\n    var res = await Request().post(\n      Api.delFavFolder,\n      data: {\n        'media_ids': mediaIds,\n        'platform': 'web',\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true};\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  // 稍后再看播放全部\n  // static Future toViewPlayAll({required int oid, required String bvid}) async {\n  //   var res = await Request().get(\n  //     Api.watchLaterHtml,\n  //     data: {\n  //       'oid': oid,\n  //       'bvid': bvid,\n  //     },\n  //   );\n  //   String scriptContent =\n  //       extractScriptContents(parse(res.data).body!.outerHtml)[0];\n  //   int startIndex = scriptContent.indexOf('{');\n  //   int endIndex = scriptContent.lastIndexOf('};');\n  //   String jsonContent = scriptContent.substring(startIndex, endIndex + 1);\n  //   // 解析JSON字符串为Map\n  //   Map<String, dynamic> jsonData = json.decode(jsonContent);\n  //   // 输出解析后的数据\n  //   return {\n  //     'status': true,\n  //     'data': jsonData['resourceList']\n  //         .map((e) => MediaVideoItemModel.fromJson(e))\n  //         .toList()\n  //   };\n  // }\n\n  static List<String> extractScriptContents(String htmlContent) {\n    RegExp scriptRegExp = RegExp(r'<script>([\\s\\S]*?)<\\/script>');\n    Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);\n    List<String> scriptContents = [];\n    for (Match match in matches) {\n      String scriptContent = match.group(1)!;\n      scriptContents.add(scriptContent);\n    }\n    return scriptContents;\n  }\n\n  // 稍后再看列表\n  static Future getMediaList({\n    required int type,\n    required int bizId,\n    required int ps,\n    int? oid,\n  }) async {\n    var res = await Request().get(\n      Api.mediaList,\n      data: {\n        'mobi_app': 'web',\n        'type': type,\n        'biz_id': bizId,\n        'oid': oid ?? '',\n        'otype': 2,\n        'ps': ps,\n        'direction': false,\n        'desc': true,\n        'sort_field': 1,\n        'tid': 0,\n        'with_current': false,\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {\n        'status': true,\n        'data': res.data['data']['media_list'] != null\n            ? res.data['data']['media_list']\n                .map<MediaVideoItemModel>(\n                    (e) => MediaVideoItemModel.fromJson(e))\n                .toList()\n            : []\n      };\n    } else {\n      return {'status': false, 'msg': res.data['message']};\n    }\n  }\n\n  // 解析收藏夹视频\n  static Future parseFavVideo({\n    required int mediaId,\n    required int oid,\n    required String bvid,\n  }) async {\n    var res = await Request().get(\n      'https://www.bilibili.com/list/ml$mediaId',\n      data: {\n        'oid': mediaId,\n        'bvid': bvid,\n      },\n    );\n    String scriptContent =\n        extractScriptContents(parse(res.data).body!.outerHtml)[0];\n    int startIndex = scriptContent.indexOf('{');\n    int endIndex = scriptContent.lastIndexOf('};');\n    String jsonContent = scriptContent.substring(startIndex, endIndex + 1);\n    // 解析JSON字符串为Map\n    Map<String, dynamic> jsonData = json.decode(jsonContent);\n    return {\n      'status': true,\n      'data': jsonData['resourceList']\n          .map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))\n          .toList()\n    };\n  }\n}\n"
  },
  {
    "path": "lib/http/video.dart",
    "content": "import 'dart:developer';\nimport 'package:hive/hive.dart';\nimport '../common/constants.dart';\nimport '../models/common/reply_type.dart';\nimport '../models/home/rcmd/result.dart';\nimport '../models/model_hot_video_item.dart';\nimport '../models/model_rec_video_item.dart';\nimport '../models/user/fav_folder.dart';\nimport '../models/video/ai.dart';\nimport '../models/video/play/url.dart';\nimport '../models/video/subTitile/result.dart';\nimport '../models/video_detail_res.dart';\nimport '../utils/recommend_filter.dart';\nimport '../utils/storage.dart';\nimport '../utils/subtitle.dart';\nimport '../utils/wbi_sign.dart';\nimport 'api.dart';\nimport 'init.dart';\n\n/// res.data['code'] == 0 请求正常返回结果\n/// res.data['data'] 为结果\n/// 返回{'status': bool, 'data': List}\n/// view层根据 status 判断渲染逻辑\nclass VideoHttp {\n  static Box localCache = GStrorage.localCache;\n  static Box setting = GStrorage.setting;\n  static bool enableRcmdDynamic =\n      setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);\n  static Box userInfoCache = GStrorage.userInfo;\n\n  // 首页推荐视频\n  static Future rcmdVideoList({required int ps, required int freshIdx}) async {\n    try {\n      var res = await Request().get(\n        Api.recommendListWeb,\n        data: {\n          'version': 1,\n          'feed_version': 'V3',\n          'homepage_ver': 1,\n          'ps': ps,\n          'fresh_idx': freshIdx,\n          'brush': freshIdx,\n          'fresh_type': 4\n        },\n      );\n      if (res.data['code'] == 0) {\n        List<RecVideoItemModel> list = [];\n        List<int> blackMidsList =\n            setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);\n        for (var i in res.data['data']['item']) {\n          //过滤掉live与ad，以及拉黑用户\n          if (i['goto'] == 'av' &&\n              (i['owner'] != null &&\n                  !blackMidsList.contains(i['owner']['mid']))) {\n            RecVideoItemModel videoItem = RecVideoItemModel.fromJson(i);\n            if (!RecommendFilter.filter(videoItem)) {\n              list.add(videoItem);\n            }\n          }\n        }\n        return {'status': true, 'data': list};\n      } else {\n        return {'status': false, 'data': [], 'msg': res.data['message']};\n      }\n    } catch (err) {\n      return {'status': false, 'data': [], 'msg': err.toString()};\n    }\n  }\n\n  // 添加额外的loginState变量模拟未登录状态\n  static Future rcmdVideoListApp(\n      {bool loginStatus = true, required int freshIdx}) async {\n    var res = await Request().get(\n      Api.recommendListApp,\n      data: {\n        'idx': freshIdx,\n        'flush': '5',\n        'column': '4',\n        'device': 'pad',\n        'device_type': 0,\n        'device_name': 'vivo',\n        'pull': freshIdx == 0 ? 'true' : 'false',\n        'appkey': Constants.appKey,\n        'access_key': loginStatus\n            ? (localCache\n                    .get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??\n                '')\n            : ''\n      },\n    );\n    if (res.data['code'] == 0) {\n      List<RecVideoItemAppModel> list = [];\n      List<int> blackMidsList =\n          setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);\n      for (var i in res.data['data']['items']) {\n        // 屏蔽推广和拉黑用户\n        if (i['card_goto'] != 'ad_av' &&\n            (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&\n            (i['args'] != null &&\n                !blackMidsList.contains(i['args']['up_mid']))) {\n          RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i);\n          if (!RecommendFilter.filter(videoItem)) {\n            list.add(videoItem);\n          }\n        }\n      }\n      return {'status': true, 'data': list};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 最热视频\n  static Future hotVideoList({required int pn, required int ps}) async {\n    try {\n      var res = await Request().get(\n        Api.hotList,\n        data: {'pn': pn, 'ps': ps},\n      );\n      if (res.data['code'] == 0) {\n        List<HotVideoItemModel> list = [];\n        List<int> blackMidsList =\n            setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);\n        for (var i in res.data['data']['list']) {\n          if (!blackMidsList.contains(i['owner']['mid'])) {\n            list.add(HotVideoItemModel.fromJson(i));\n          }\n        }\n        return {'status': true, 'data': list};\n      } else {\n        return {'status': false, 'data': [], 'msg': res.data['message']};\n      }\n    } catch (err) {\n      return {'status': false, 'data': [], 'msg': err};\n    }\n  }\n\n  // 视频流\n  static Future videoUrl(\n      {int? avid, String? bvid, required int cid, int? qn}) async {\n    Map<String, dynamic> data = {\n      'cid': cid,\n      'qn': qn ?? 80,\n      // 获取所有格式的视频\n      'fnval': 4048,\n    };\n    if (avid != null) {\n      data['avid'] = avid;\n    }\n    if (bvid != null) {\n      data['bvid'] = bvid;\n    }\n\n    // 免登录查看1080p\n    if (userInfoCache.get('userInfoCache') == null &&\n        setting.get(SettingBoxKey.p1080, defaultValue: true)) {\n      data['try_look'] = 1;\n    }\n\n    Map params = await WbiSign().makSign({\n      ...data,\n      'fourk': 1,\n      'voice_balance': 1,\n      'gaia_source': 'pre-load',\n      'web_location': 1550101,\n    });\n\n    try {\n      var res = await Request().get(Api.videoUrl, data: params);\n      if (res.data['code'] == 0) {\n        return {\n          'status': true,\n          'data': PlayUrlModel.fromJson(res.data['data'])\n        };\n      } else {\n        return {\n          'status': false,\n          'data': [],\n          'code': res.data['code'],\n          'msg': res.data['message'],\n        };\n      }\n    } catch (err) {\n      return {'status': false, 'data': [], 'msg': err};\n    }\n  }\n\n  // 视频信息 标题、简介\n  static Future videoIntro({required String bvid}) async {\n    var res = await Request().get(Api.videoIntro, data: {'bvid': bvid});\n    if (res.data['code'] == 0) {\n      VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);\n      return {'status': true, 'data': result.data!};\n    } else {\n      return {\n        'status': false,\n        'data': null,\n        'code': res.data['code'],\n        'msg': res.data['message'],\n      };\n    }\n  }\n\n  // 相关视频\n  static Future relatedVideoList({required String bvid}) async {\n    var res = await Request().get(Api.relatedList, data: {'bvid': bvid});\n    if (res.data['code'] == 0) {\n      List<HotVideoItemModel> list = [];\n      for (var i in res.data['data']) {\n        HotVideoItemModel videoItem = HotVideoItemModel.fromJson(i);\n        if (!RecommendFilter.filter(videoItem, relatedVideos: true)) {\n          list.add(videoItem);\n        }\n      }\n      return {'status': true, 'data': list};\n    } else {\n      return {'status': false, 'data': []};\n    }\n  }\n\n  // 获取点赞状态\n  static Future hasLikeVideo({required String bvid}) async {\n    var res = await Request().get(Api.hasLikeVideo, data: {'bvid': bvid});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': []};\n    }\n  }\n\n  // 获取投币状态\n  static Future hasCoinVideo({required String bvid}) async {\n    var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid});\n    print('res: $res');\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': []};\n    }\n  }\n\n  // 投币\n  static Future coinVideo({required String bvid, required int multiply}) async {\n    var res = await Request().post(\n      Api.coinVideo,\n      data: {\n        'bvid': bvid,\n        'multiply': multiply,\n        'select_like': 0,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 获取收藏状态\n  static Future hasFavVideo({required int aid}) async {\n    var res = await Request().get(Api.hasFavVideo, data: {'aid': aid});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': []};\n    }\n  }\n\n  // 一键三连\n  static Future oneThree({required String bvid}) async {\n    var res = await Request().post(\n      Api.oneThree,\n      data: {\n        'bvid': bvid,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // （取消）点赞\n  static Future likeVideo({required String bvid, required bool type}) async {\n    var res = await Request().post(\n      Api.likeVideo,\n      data: {\n        'bvid': bvid,\n        'like': type ? 1 : 2,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // （取消）收藏\n  static Future favVideo(\n      {required int aid, String? addIds, String? delIds}) async {\n    var res = await Request().post(\n      Api.favVideo,\n      data: {\n        'rid': aid,\n        'type': 2,\n        'add_media_ids': addIds ?? '',\n        'del_media_ids': delIds ?? '',\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 查看视频被收藏在哪个文件夹\n  static Future videoInFolder({required int mid, required int rid}) async {\n    var res = await Request()\n        .get(Api.videoInFolder, data: {'up_mid': mid, 'rid': rid});\n    if (res.data['code'] == 0) {\n      FavFolderData data = FavFolderData.fromJson(res.data['data']);\n      return {'status': true, 'data': data};\n    } else {\n      return {'status': false, 'data': []};\n    }\n  }\n\n  // 发表评论 replyAdd\n\n  // type\tnum\t评论区类型代码\t必要\t类型代码见表\n  // oid\tnum\t目标评论区id\t必要\n  // root\tnum\t根评论rpid\t非必要\t二级评论以上使用\n  // parent\tnum\t父评论rpid\t非必要\t二级评论同根评论id 大于二级评论为要回复的评论id\n  // message\tstr\t发送评论内容\t必要\t最大1000字符\n  // plat\tnum\t发送平台标识\t非必要\t1：web端 2：安卓客户端  3：ios客户端  4：wp客户端\n  static Future replyAdd({\n    required ReplyType type,\n    required int oid,\n    required String message,\n    int? root,\n    int? parent,\n  }) async {\n    if (message == '') {\n      return {'status': false, 'data': [], 'msg': '请输入评论内容'};\n    }\n    var res = await Request().post(\n      Api.replyAdd,\n      data: {\n        'type': type.index,\n        'oid': oid,\n        'root': root == null || root == 0 ? '' : root,\n        'parent': parent == null || parent == 0 ? '' : parent,\n        'message': message,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    log(res.toString());\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 查询是否关注up\n  static Future hasFollow({required int mid}) async {\n    var res = await Request().get(Api.hasFollow, data: {'fid': mid});\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': []};\n    }\n  }\n\n  // 操作用户关系\n  static Future relationMod(\n      {required int mid, required int act, required int reSrc}) async {\n    var res = await Request().post(\n      Api.relationMod,\n      data: {\n        'fid': mid,\n        'act': act,\n        're_src': reSrc,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      if (act == 5) {\n        List<int> blackMidsList =\n            setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);\n        blackMidsList.add(mid);\n        setting.put(SettingBoxKey.blackMidsList, blackMidsList);\n      }\n      return {'status': true, 'data': res.data['data'], 'msg': '成功'};\n    } else {\n      return {'status': false, 'data': [], 'msg': res.data['message']};\n    }\n  }\n\n  // 视频播放进度\n  static Future heartBeat({bvid, cid, progress, realtime}) async {\n    await Request().post(\n      Api.heartBeat,\n      data: {\n        // 'aid': aid,\n        'bvid': bvid,\n        'cid': cid,\n        // 'epid': '',\n        // 'sid': '',\n        // 'mid': '',\n        'played_time': progress,\n        // 'realtime': realtime,\n        // 'type': '',\n        // 'sub_type': '',\n        'csrf': await Request.getCsrf(),\n      },\n    );\n  }\n\n  // 添加追番\n  static Future bangumiAdd({int? seasonId}) async {\n    var res = await Request().post(\n      Api.bangumiAdd,\n      data: {\n        'season_id': seasonId,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'msg': res.data['result']['toast']};\n    } else {\n      return {'status': false, 'msg': res.data['result']['toast']};\n    }\n  }\n\n  // 取消追番\n  static Future bangumiDel({int? seasonId}) async {\n    var res = await Request().post(\n      Api.bangumiDel,\n      data: {\n        'season_id': seasonId,\n        'csrf': await Request.getCsrf(),\n      },\n    );\n    if (res.data['code'] == 0) {\n      return {'status': true, 'msg': res.data['result']['toast']};\n    } else {\n      return {'status': false, 'msg': res.data['result']['toast']};\n    }\n  }\n\n  // 查看视频同时在看人数\n  static Future onlineTotal({int? aid, String? bvid, int? cid}) async {\n    var res = await Request().get(Api.onlineTotal, data: {\n      'aid': aid,\n      'bvid': bvid,\n      'cid': cid,\n    });\n    if (res.data['code'] == 0) {\n      return {'status': true, 'data': res.data['data']};\n    } else {\n      return {'status': false, 'data': null, 'msg': res.data['message']};\n    }\n  }\n\n  static Future aiConclusion({\n    String? bvid,\n    int? cid,\n    int? upMid,\n  }) async {\n    Map params = await WbiSign().makSign({\n      'bvid': bvid,\n      'cid': cid,\n      'up_mid': upMid,\n    });\n    var res = await Request().get(Api.aiConclusion, data: params);\n    if (res.data['code'] == 0 && res.data['data']['code'] == 0) {\n      return {\n        'status': true,\n        'data': AiConclusionModel.fromJson(res.data['data']),\n      };\n    } else {\n      return {'status': false, 'data': []};\n    }\n  }\n\n  static Future getSubtitle({int? cid, String? bvid}) async {\n    var res = await Request().get(Api.getSubtitleConfig, data: {\n      'cid': cid,\n      'bvid': bvid,\n    });\n    try {\n      if (res.data['code'] == 0) {\n        return {\n          'status': true,\n          'data': SubTitlteModel.fromJson(res.data['data']),\n        };\n      } else {\n        return {'status': false, 'data': [], 'msg': res.data['msg']};\n      }\n    } catch (err) {\n      return {'status': false, 'data': [], 'msg': res.data['msg']};\n    }\n  }\n\n  // 视频排行\n  static Future getRankVideoList(int rid) async {\n    try {\n      var rankApi = \"${Api.getRankApi}?rid=$rid&type=all\";\n      var res = await Request().get(rankApi);\n      if (res.data['code'] == 0) {\n        List<HotVideoItemModel> list = [];\n        List<int> blackMidsList =\n            setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);\n        for (var i in res.data['data']['list']) {\n          if (!blackMidsList.contains(i['owner']['mid'])) {\n            list.add(HotVideoItemModel.fromJson(i));\n          }\n        }\n        return {'status': true, 'data': list};\n      } else {\n        return {'status': false, 'data': [], 'msg': res.data['message']};\n      }\n    } catch (err) {\n      return {'status': false, 'data': [], 'msg': err};\n    }\n  }\n\n  // 获取字幕内容\n  static Future<Map<String, dynamic>> getSubtitleContent(url) async {\n    var res = await Request().get('https:$url');\n    final String content =\n        await SubTitleUtils.convertToWebVTT(res.data['body']);\n    final List body = res.data['body'];\n    return {'content': content, 'body': body};\n  }\n}\n"
  },
  {
    "path": "lib/main.dart",
    "content": "import 'dart:io';\n\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_displaymode/flutter_displaymode.dart';\nimport 'package:flutter_localizations/flutter_localizations.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:dynamic_color/dynamic_color.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/widgets/custom_toast.dart';\nimport 'package:pilipala/http/init.dart';\nimport 'package:pilipala/models/common/color_type.dart';\nimport 'package:pilipala/models/common/theme_type.dart';\nimport 'package:pilipala/pages/search/index.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/router/app_pages.dart';\nimport 'package:pilipala/pages/main/view.dart';\nimport 'package:pilipala/services/service_locator.dart';\nimport 'package:pilipala/utils/app_scheme.dart';\nimport 'package:pilipala/utils/data.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:media_kit/media_kit.dart';\nimport 'package:pilipala/utils/recommend_filter.dart';\nimport 'package:catcher_2/catcher_2.dart';\nimport './services/loggeer.dart';\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  MediaKit.ensureInitialized();\n  await SystemChrome.setPreferredOrientations(\n      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);\n  await GStrorage.init();\n  clearLogs();\n  Request();\n  await Request.setCookie();\n\n  // 异常捕获 logo记录\n  final Catcher2Options releaseConfig = Catcher2Options(\n    SilentReportMode(),\n    [FileHandler(await getLogsPath())],\n  );\n\n  Catcher2(\n    releaseConfig: releaseConfig,\n    runAppFunction: () {\n      runApp(const MyApp());\n    },\n  );\n\n  // 小白条、导航栏沉浸\n  if (Platform.isAndroid) {\n    final androidInfo = await DeviceInfoPlugin().androidInfo;\n    if (androidInfo.version.sdkInt >= 29) {\n      SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);\n    }\n    SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(\n      systemNavigationBarColor: Colors.transparent,\n      systemNavigationBarDividerColor: Colors.transparent,\n      statusBarColor: Colors.transparent,\n    ));\n  }\n\n  PiliSchame.init();\n  await GlobalDataCache().initialize();\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    Box setting = GStrorage.setting;\n    // 主题色\n    Color defaultColor =\n        colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]\n            ['color'];\n    Color brandColor = defaultColor;\n    // 主题模式\n    ThemeType currentThemeValue = ThemeType.values[setting\n        .get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)];\n    // 是否动态取色\n    bool isDynamicColor =\n        setting.get(SettingBoxKey.dynamicColor, defaultValue: true);\n    // 字体缩放大小\n    double textScale =\n        setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);\n\n    // 强制设置高帧率\n    if (Platform.isAndroid) {\n      try {\n        late List modes;\n        FlutterDisplayMode.supported.then((value) {\n          modes = value;\n          var storageDisplay = setting.get(SettingBoxKey.displayMode);\n          DisplayMode f = DisplayMode.auto;\n          if (storageDisplay != null) {\n            f = modes.firstWhere((e) => e.toString() == storageDisplay);\n          }\n          DisplayMode preferred = modes.toList().firstWhere((el) => el == f);\n          FlutterDisplayMode.setPreferredMode(preferred);\n        });\n      } catch (_) {}\n    }\n\n    if (Platform.isAndroid) {\n      return AndroidApp(\n        brandColor: brandColor,\n        isDynamicColor: isDynamicColor,\n        currentThemeValue: currentThemeValue,\n        textScale: textScale,\n      );\n    } else {\n      return OtherApp(\n        brandColor: brandColor,\n        currentThemeValue: currentThemeValue,\n        textScale: textScale,\n      );\n    }\n  }\n}\n\nclass AndroidApp extends StatelessWidget {\n  const AndroidApp({\n    super.key,\n    required this.brandColor,\n    required this.isDynamicColor,\n    required this.currentThemeValue,\n    required this.textScale,\n  });\n\n  final Color brandColor;\n  final bool isDynamicColor;\n  final ThemeType currentThemeValue;\n  final double textScale;\n\n  @override\n  Widget build(BuildContext context) {\n    return DynamicColorBuilder(\n      builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {\n        ColorScheme? lightColorScheme;\n        ColorScheme? darkColorScheme;\n        if (lightDynamic != null && darkDynamic != null && isDynamicColor) {\n          // dynamic取色成功\n          lightColorScheme = lightDynamic.harmonized();\n          darkColorScheme = darkDynamic.harmonized();\n        } else {\n          // dynamic取色失败，采用品牌色\n          lightColorScheme = ColorScheme.fromSeed(\n            seedColor: brandColor,\n            brightness: Brightness.light,\n          );\n          darkColorScheme = ColorScheme.fromSeed(\n            seedColor: brandColor,\n            brightness: Brightness.dark,\n          );\n        }\n        return BuildMainApp(\n          lightColorScheme: lightColorScheme,\n          darkColorScheme: darkColorScheme,\n          currentThemeValue: currentThemeValue,\n          textScale: textScale,\n        );\n      }),\n    );\n  }\n}\n\nclass OtherApp extends StatelessWidget {\n  const OtherApp({\n    super.key,\n    required this.brandColor,\n    required this.currentThemeValue,\n    required this.textScale,\n  });\n\n  final Color brandColor;\n  final ThemeType currentThemeValue;\n  final double textScale;\n\n  @override\n  Widget build(BuildContext context) {\n    return BuildMainApp(\n      lightColorScheme: ColorScheme.fromSeed(\n        seedColor: brandColor,\n        brightness: Brightness.light,\n      ),\n      darkColorScheme: ColorScheme.fromSeed(\n        seedColor: brandColor,\n        brightness: Brightness.dark,\n      ),\n      currentThemeValue: currentThemeValue,\n      textScale: textScale,\n    );\n  }\n}\n\nclass BuildMainApp extends StatelessWidget {\n  const BuildMainApp({\n    super.key,\n    required this.lightColorScheme,\n    required this.darkColorScheme,\n    required this.currentThemeValue,\n    required this.textScale,\n  });\n\n  final ColorScheme lightColorScheme;\n  final ColorScheme darkColorScheme;\n  final ThemeType currentThemeValue;\n  final double textScale;\n\n  @override\n  Widget build(BuildContext context) {\n    final SnackBarThemeData snackBarTheme = SnackBarThemeData(\n      actionTextColor: lightColorScheme.primary,\n      backgroundColor: lightColorScheme.secondaryContainer,\n      closeIconColor: lightColorScheme.secondary,\n      contentTextStyle: TextStyle(color: lightColorScheme.secondary),\n      elevation: 20,\n    );\n\n    return GetMaterialApp(\n      title: 'PiliPala',\n      theme: ThemeData(\n        colorScheme: currentThemeValue == ThemeType.dark\n            ? darkColorScheme\n            : lightColorScheme,\n        snackBarTheme: snackBarTheme,\n        pageTransitionsTheme: const PageTransitionsTheme(\n          builders: <TargetPlatform, PageTransitionsBuilder>{\n            TargetPlatform.android: ZoomPageTransitionsBuilder(\n              allowEnterRouteSnapshotting: false,\n            ),\n          },\n        ),\n      ),\n      darkTheme: ThemeData(\n        colorScheme: currentThemeValue == ThemeType.light\n            ? lightColorScheme\n            : darkColorScheme,\n        snackBarTheme: snackBarTheme,\n      ),\n      localizationsDelegates: const [\n        GlobalCupertinoLocalizations.delegate,\n        GlobalMaterialLocalizations.delegate,\n        GlobalWidgetsLocalizations.delegate,\n      ],\n      locale: const Locale(\"zh\", \"CN\"),\n      supportedLocales: const [Locale(\"zh\", \"CN\"), Locale(\"en\", \"US\")],\n      fallbackLocale: const Locale(\"zh\", \"CN\"),\n      getPages: Routes.getPages,\n      home: const MainApp(),\n      builder: (BuildContext context, Widget? child) {\n        return FlutterSmartDialog(\n          toastBuilder: (String msg) => CustomToast(msg: msg),\n          child: MediaQuery(\n            data: MediaQuery.of(context)\n                .copyWith(textScaler: TextScaler.linear(textScale)),\n            child: child!,\n          ),\n        );\n      },\n      navigatorObservers: [\n        VideoDetailPage.routeObserver,\n        SearchPage.routeObserver,\n      ],\n      onReady: () async {\n        RecommendFilter();\n        Data.init();\n        setupServiceLocator();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/models/bangumi/info.dart",
    "content": "class BangumiInfoModel {\n  BangumiInfoModel({\n    this.activity,\n    this.actors,\n    this.alias,\n    this.areas,\n    this.bkgCover,\n    this.cover,\n    this.enableVt,\n    this.episodes,\n    this.evaluate,\n    this.freya,\n    this.jpTitle,\n    this.link,\n    this.mediaId,\n    this.newEp,\n    this.playStrategy,\n    this.positive,\n    this.publish,\n    this.rating,\n    this.record,\n    this.rights,\n    this.seasonId,\n    this.seasonTitle,\n    this.seasons,\n    this.series,\n    this.shareCopy,\n    this.shareSubTitle,\n    this.shareUrl,\n    this.show,\n    this.showSeasonType,\n    this.squareCover,\n    this.stat,\n    this.status,\n    this.styles,\n    this.subTitle,\n    this.title,\n    this.total,\n    this.type,\n    this.userStatus,\n    this.staff,\n  });\n\n  Map? activity;\n  String? actors;\n  String? alias;\n  List? areas;\n  String? bkgCover;\n  String? cover;\n  String? enableVt;\n  List<EpisodeItem>? episodes;\n  String? evaluate;\n  Map? freya;\n  String? jpTitle;\n  String? link;\n  int? mediaId;\n  Map? newEp;\n  Map? playStrategy;\n  Map? positive;\n  Map? publish;\n  Map? rating;\n  String? record;\n  Map? rights;\n  int? seasonId;\n  String? seasonTitle;\n  List? seasons;\n  Map? series;\n  String? shareCopy;\n  String? shareSubTitle;\n  String? shareUrl;\n  Map? show;\n  int? showSeasonType;\n  String? squareCover;\n  Map? stat;\n  int? status;\n  List? styles;\n  String? subTitle;\n  String? title;\n  int? total;\n  int? type;\n  Map? userStatus;\n  String? staff;\n\n  BangumiInfoModel.fromJson(Map<String, dynamic> json) {\n    activity = json['activity'];\n    actors = json['actors'];\n    alias = json['alias'];\n    areas = json['areas'];\n    bkgCover = json['bkg_cover'];\n    cover = json['cover'];\n    enableVt = json['enableVt'];\n    episodes = json['episodes']\n        .map<EpisodeItem>((e) => EpisodeItem.fromJson(e))\n        .toList();\n    evaluate = json['evaluate'];\n    freya = json['freya'];\n    jpTitle = json['jp_title'];\n    link = json['link'];\n    mediaId = json['media_id'];\n    newEp = json['new_ep'];\n    playStrategy = json['play_strategy'];\n    positive = json['positive'];\n    publish = json['publish'];\n    rating = json['rating'];\n    record = json['record'];\n    rights = json['rights'];\n    seasonId = json['season_id'];\n    seasonTitle = json['season_title'];\n    seasons = json['seasons'];\n    series = json['series'];\n    shareCopy = json['share_copy'];\n    shareSubTitle = json['share_sub_title'];\n    shareUrl = json['share_url'];\n    show = json['show'];\n    showSeasonType = json['show_season_type'];\n    squareCover = json['square_cover'];\n    stat = json['stat'];\n    status = json['status'];\n    styles = json['styles'];\n    subTitle = json['sub_title'];\n    title = json['title'];\n    total = json['total'];\n    type = json['type'];\n    userStatus = json['user_status'];\n    staff = json['staff'];\n  }\n}\n\nclass EpisodeItem {\n  EpisodeItem({\n    this.aid,\n    this.badge,\n    this.badgeInfo,\n    this.badgeType,\n    this.bvid,\n    this.cid,\n    this.cover,\n    this.dimension,\n    this.duration,\n    this.enableVt,\n    this.from,\n    this.id,\n    this.isViewHide,\n    this.link,\n    this.longTitle,\n    this.pubTime,\n    this.pv,\n    this.releaseDate,\n    this.rights,\n    this.shareCopy,\n    this.shareUrl,\n    this.shortLink,\n    this.skip,\n    this.status,\n    this.subtitle,\n    this.title,\n    this.vid,\n  });\n\n  int? aid;\n  String? badge;\n  Map? badgeInfo;\n  int? badgeType;\n  String? bvid;\n  int? cid;\n  String? cover;\n  Map? dimension;\n  int? duration;\n  bool? enableVt;\n  String? from;\n  int? id;\n  bool? isViewHide;\n  String? link;\n  String? longTitle;\n  int? pubTime;\n  int? pv;\n  String? releaseDate;\n  Map? rights;\n  String? shareCopy;\n  String? shareUrl;\n  String? shortLink;\n  Map? skip;\n  int? status;\n  String? subtitle;\n  String? title;\n  String? vid;\n\n  EpisodeItem.fromJson(Map<String, dynamic> json) {\n    aid = json['aid'];\n    badge = json['badge'] != '' ? json['badge'] : null;\n    badgeInfo = json['badge_info'];\n    badgeType = json['badge_type'];\n    bvid = json['bvid'];\n    cid = json['cid'];\n    cover = json['cover'];\n    dimension = json['dimension'];\n    duration = json['duration'];\n    enableVt = json['enable_vt'];\n    from = json['from'];\n    id = json['id'];\n    isViewHide = json['is_view_hide'];\n    link = json['link'];\n    longTitle = json['long_title'];\n    pubTime = json['pub_time'];\n    pv = json['pv'];\n    releaseDate = json['release_date'];\n    rights = json['rights'];\n    shareCopy = json['share_copy'];\n    shareUrl = json['share_url'];\n    shortLink = json['short_link'];\n    skip = json['skip'];\n    status = json['status'];\n    subtitle = json['subtitle'];\n    title = json['title'];\n    vid = json['vid'];\n  }\n}\n"
  },
  {
    "path": "lib/models/bangumi/list.dart",
    "content": "class BangumiListDataModel {\n  BangumiListDataModel({\n    this.hasNext,\n    this.list,\n    this.num,\n    this.size,\n    this.total,\n  });\n\n  int? hasNext;\n  List? list;\n  int? num;\n  int? size;\n  int? total;\n\n  BangumiListDataModel.fromJson(Map<String, dynamic> json) {\n    hasNext = json['has_next'];\n    list = json['list'] != null\n        ? json['list']\n            .map<BangumiListItemModel>((e) => BangumiListItemModel.fromJson(e))\n            .toList()\n        : [];\n    num = json['num'];\n    size = json['size'];\n    total = json['total'];\n  }\n}\n\nclass BangumiListItemModel {\n  BangumiListItemModel({\n    this.badge,\n    this.badgeType,\n    this.pic,\n    this.cover,\n    // this.firstEp,\n    this.indexShow,\n    this.isFinish,\n    this.link,\n    this.mediaId,\n    this.order,\n    this.orderType,\n    this.score,\n    this.seasonId,\n    this.seaconStatus,\n    this.seasonType,\n    this.subTitle,\n    this.title,\n    this.titleIcon,\n    this.progress,\n  });\n\n  String? badge;\n  int? badgeType;\n  String? pic;\n  String? cover;\n  String? indexShow;\n  int? isFinish;\n  String? link;\n  int? mediaId;\n  String? order;\n  String? orderType;\n  String? score;\n  int? seasonId;\n  int? seaconStatus;\n  int? seasonType;\n  String? subTitle;\n  String? title;\n  String? titleIcon;\n\n  String? progress;\n\n  BangumiListItemModel.fromJson(Map<String, dynamic> json) {\n    badge = json['badge'] == '' ? null : json['badge'];\n    badgeType = json['badge_type'];\n    pic = json['cover'];\n    cover = json['cover'];\n    indexShow = json['index_show'];\n    isFinish = json['is_finish'];\n    link = json['link'];\n    mediaId = json['media_id'];\n    order = json['order'];\n    orderType = json['order_type'];\n    score = json['score'];\n    seasonId = json['season_id'];\n    seaconStatus = json['seacon_status'];\n    seasonType = json['season_type'];\n    subTitle = json['sub_title'];\n    title = json['title'];\n    titleIcon = json['title_icon'];\n\n    progress = json['progress'];\n  }\n}\n"
  },
  {
    "path": "lib/models/common/action_type.dart",
    "content": "// 操作类型的枚举值：点赞 不喜欢 收藏 投币 稍后再看 下载封面 后台播放 听视频 分享 下载视频\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\n\nenum ActionType {\n  like,\n  coin,\n  collect,\n  watchLater,\n  share,\n  dislike,\n  downloadCover,\n  copyLink,\n  // backgroundPlay,\n  // listenVideo,\n  // downloadVideo,\n}\n\nextension ActionTypeExtension on ActionType {\n  String get value => [\n        'like',\n        'coin',\n        'collect',\n        'watchLater',\n        'share',\n        'dislike',\n        'downloadCover',\n        'copyLink',\n        // 'backgroundPlay',\n        // 'listenVideo',\n        // 'downloadVideo',\n      ][index];\n  String get label => [\n        '点赞视频',\n        '投币',\n        '收藏视频',\n        '稍后再看',\n        '视频分享',\n        '不喜欢',\n        '下载封面',\n        '复制链接',\n        // '后台播放',\n        // '听视频',\n        // '下载视频',\n      ][index];\n}\n\nList<Map> actionMenuConfig = [\n  {\n    'icon': const Icon(Icons.thumb_up_alt_outlined),\n    'label': '点赞视频',\n    'value': ActionType.like,\n  },\n  {\n    'icon': Image.asset(\n      'assets/images/coin.png',\n      width: 26,\n      color: IconTheme.of(Get.context!).color!.withOpacity(0.65),\n    ),\n    'label': '投币',\n    'value': ActionType.coin,\n  },\n  {\n    'icon': const Icon(Icons.star_border),\n    'label': '收藏视频',\n    'value': ActionType.collect,\n  },\n  {\n    'icon': const Icon(Icons.watch_later_outlined),\n    'label': '稍后再看',\n    'value': ActionType.watchLater,\n  },\n  {\n    'icon': const Icon(Icons.share),\n    'label': '视频分享',\n    'value': ActionType.share,\n  },\n  {\n    'icon': const Icon(Icons.thumb_down_alt_outlined),\n    'label': '不喜欢',\n    'value': ActionType.dislike,\n  },\n  {\n    'icon': const Icon(Icons.image_outlined),\n    'label': '下载封面',\n    'value': ActionType.downloadCover,\n  },\n  {\n    'icon': const Icon(Icons.link_outlined),\n    'label': '复制链接',\n    'value': ActionType.copyLink,\n  },\n];\n"
  },
  {
    "path": "lib/models/common/business_type.dart",
    "content": "enum BusinessType {\n  // 普通视频\n  archive,\n  // 剧集（番剧 / 影视）\n  pgc,\n  // 直播\n  live,\n  // 文章\n  articleList,\n  // 文章\n  article,\n  hiddenDurationType,\n  showBadge\n}\n\nextension BusinessTypeExtension on BusinessType {\n  String get type =>\n      ['archive', 'pgc', 'live', 'article-list', 'article'][index];\n  // 隐藏时长\n  List get hiddenDurationType => ['live', 'article-list', 'article'];\n  // 右上\n  List get showBadge => ['pgc', 'article-list', 'article'];\n}\n"
  },
  {
    "path": "lib/models/common/color_type.dart",
    "content": "import 'package:flutter/material.dart';\n\nfinal List<Map<String, dynamic>> colorThemeTypes = [\n  {'color': const Color.fromARGB(255, 92, 182, 123), 'label': '默认绿'},\n  {'color': Colors.pink, 'label': '粉红色'},\n  {'color': Colors.red, 'label': '红色'},\n  {'color': Colors.orange, 'label': '橙色'},\n  {'color': Colors.amber, 'label': '琥珀色'},\n  {'color': Colors.yellow, 'label': '黄色'},\n  {'color': Colors.lime, 'label': '酸橙色'},\n  {'color': Colors.lightGreen, 'label': '浅绿色'},\n  {'color': Colors.green, 'label': '绿色'},\n  {'color': Colors.teal, 'label': '青色'},\n  {'color': Colors.cyan, 'label': '蓝绿色'},\n  {'color': Colors.lightBlue, 'label': '浅蓝色'},\n  {'color': Colors.blue, 'label': '蓝色'},\n  {'color': Colors.indigo, 'label': '靛蓝色'},\n  {'color': Colors.purple, 'label': '紫色'},\n  {'color': Colors.deepPurple, 'label': '深紫色'},\n  {'color': Colors.blueGrey, 'label': '蓝灰色'},\n  {'color': Colors.brown, 'label': '棕色'},\n  {'color': Colors.grey, 'label': '灰色'},\n];\n"
  },
  {
    "path": "lib/models/common/dynamic_badge_mode.dart",
    "content": "enum DynamicBadgeMode { hidden, point, number }\n\nextension DynamicBadgeModeDesc on DynamicBadgeMode {\n  String get description => ['隐藏', '红点', '数字'][index];\n}\n\nextension DynamicBadgeModeCode on DynamicBadgeMode {\n  int get code => [0, 1, 2][index];\n}\n"
  },
  {
    "path": "lib/models/common/dynamics_type.dart",
    "content": "enum DynamicsType {\n  all,\n  video,\n  pgc,\n  article,\n}\n\nextension BusinessTypeExtension on DynamicsType {\n  String get values => ['all', 'video', 'pgc', 'article'][index];\n  String get labels => ['全部', '投稿', '番剧', '专栏'][index];\n}\n"
  },
  {
    "path": "lib/models/common/gesture_mode.dart",
    "content": "enum FullScreenGestureMode {\n  /// 从上滑到下\n  fromToptoBottom,\n\n  /// 从下滑到上\n  fromBottomtoTop,\n}\n\nextension FullScreenGestureModeExtension on FullScreenGestureMode {\n  String get values => ['fromToptoBottom', 'fromBottomtoTop'][index];\n  String get labels => ['从上往下滑进入全屏', '从下往上滑进入全屏'][index];\n}\n"
  },
  {
    "path": "lib/models/common/index.dart",
    "content": "library commonn_model;\n\nexport './business_type.dart';\nexport './gesture_mode.dart';\n"
  },
  {
    "path": "lib/models/common/nav_bar_config.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport '../../pages/dynamics/index.dart';\nimport '../../pages/home/index.dart';\nimport '../../pages/media/index.dart';\nimport '../../pages/rank/index.dart';\n\nList defaultNavigationBars = [\n  {\n    'id': 0,\n    'icon': const Icon(\n      Icons.home_outlined,\n      size: 21,\n    ),\n    'selectIcon': const Icon(\n      Icons.home,\n      size: 21,\n    ),\n    'label': \"首页\",\n    'count': 0,\n    'page': const HomePage(),\n  },\n  {\n    'id': 1,\n    'icon': const Icon(\n      Icons.trending_up,\n      size: 21,\n    ),\n    'selectIcon': const Icon(\n      Icons.trending_up_outlined,\n      size: 21,\n    ),\n    'label': \"排行榜\",\n    'count': 0,\n    'page': const RankPage(),\n  },\n  {\n    'id': 2,\n    'icon': const Icon(\n      Icons.motion_photos_on_outlined,\n      size: 21,\n    ),\n    'selectIcon': const Icon(\n      Icons.motion_photos_on,\n      size: 21,\n    ),\n    'label': \"动态\",\n    'count': 0,\n    'page': const DynamicsPage(),\n  },\n  {\n    'id': 3,\n    'icon': const Icon(\n      Icons.video_collection_outlined,\n      size: 20,\n    ),\n    'selectIcon': const Icon(\n      Icons.video_collection,\n      size: 21,\n    ),\n    'label': \"媒体库\",\n    'count': 0,\n    'page': const MediaPage(),\n  }\n];\n"
  },
  {
    "path": "lib/models/common/rank_type.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/pages/rank/zone/index.dart';\n\nenum RandType {\n  all,\n  animation,\n  music,\n  dance,\n  game,\n  knowledge,\n  technology,\n  sport,\n  car,\n  food,\n  animal,\n  madness,\n  fashion,\n  entertainment,\n  film\n}\n\nextension RankTypeDesc on RandType {\n  String get description => [\n        '全站',\n        '动画',\n        '音乐',\n        '舞蹈',\n        '游戏',\n        '知识',\n        '科技',\n        '运动',\n        '汽车',\n        '美食',\n        '动物圈',\n        '鬼畜',\n        '时尚',\n        '娱乐',\n        '影视'\n      ][index];\n\n  String get id => [\n        'all',\n        'animation',\n        'music',\n        'dance',\n        'game',\n        'knowledge',\n        'technology',\n        'sport',\n        'car',\n        'food',\n        'animal',\n        'madness',\n        'fashion',\n        'entertainment',\n        'film'\n      ][index];\n}\n\nList tabsConfig = [\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '全站',\n    'type': RandType.all,\n    'page': const ZonePage(rid: 0),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '动画',\n    'type': RandType.animation,\n    'page': const ZonePage(rid: 1005),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '音乐',\n    'type': RandType.music,\n    'page': const ZonePage(rid: 1003),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '舞蹈',\n    'type': RandType.dance,\n    'page': const ZonePage(rid: 1004),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '游戏',\n    'type': RandType.game,\n    'page': const ZonePage(rid: 1008),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '知识',\n    'type': RandType.knowledge,\n    'page': const ZonePage(rid: 1010),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '科技',\n    'type': RandType.technology,\n    'page': const ZonePage(rid: 1012),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '运动',\n    'type': RandType.sport,\n    'page': const ZonePage(rid: 1018),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '汽车',\n    'type': RandType.car,\n    'page': const ZonePage(rid: 1013),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '美食',\n    'type': RandType.food,\n    'page': const ZonePage(rid: 1020),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '动物圈',\n    'type': RandType.animal,\n    'page': const ZonePage(rid: 1024),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '鬼畜',\n    'type': RandType.madness,\n    'page': const ZonePage(rid: 1007),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '时尚',\n    'type': RandType.fashion,\n    'page': const ZonePage(rid: 1014),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '娱乐',\n    'type': RandType.entertainment,\n    'page': const ZonePage(rid: 1002),\n  },\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '影视',\n    'type': RandType.film,\n    'page': const ZonePage(rid: 1001),\n  }\n];\n"
  },
  {
    "path": "lib/models/common/rcmd_type.dart",
    "content": "// 首页推荐类型\nenum RcmdType { web, app, notLogin }\n\nextension RcmdTypeExtension on RcmdType {\n  String get values => ['web', 'app', 'notLogin'][index];\n  String get labels => ['web端', 'app端', '游客模式'][index];\n}\n"
  },
  {
    "path": "lib/models/common/reply_sort_type.dart",
    "content": "enum ReplySortType { time, like }\n\nextension ReplySortTypeExtension on ReplySortType {\n  String get titles => ['最新评论', '最热评论'][index];\n  String get labels => ['最新', '最热'][index];\n}\n"
  },
  {
    "path": "lib/models/common/reply_type.dart",
    "content": "enum ReplyType {\n  unset,\n  // 视频\n  video,\n  // 话题\n  topic,\n  //\n  unset2,\n  // 活动\n  activity,\n  // 小视频\n  videoS,\n  // 小黑屋封禁信息\n  blockMsg,\n  // 公告信息\n  publicMsg,\n  // 直播活动\n  liveActivity,\n  // 活动稿件\n  activityFile,\n  // 直播公告\n  livePublic,\n  // 相簿\n  album,\n  // 专栏\n  column,\n  // 票务\n  ticket,\n  // 音频\n  audio,\n  // 风纪委员会\n  unset3,\n  // 点评\n  comment,\n  // 动态\n  dynamics,\n  // 播单\n  playList,\n  // 音乐播单\n  musicPlayList,\n  // 漫画\n  comics1,\n  // 漫画\n  comics2,\n  // 漫画\n  comics3,\n  // 课程\n  course,\n}\n"
  },
  {
    "path": "lib/models/common/search_type.dart",
    "content": "// ignore_for_file: constant_identifier_names\nenum SearchType {\n  // 视频：video\n  video,\n  // 番剧：media_bangumi,\n  media_bangumi,\n  // 影视：media_ft\n  // media_ft,\n  // 直播间及主播：live\n  // live,\n  // 直播间：live_room\n  live_room,\n  // 主播：live_user\n  // live_user,\n  // 话题：topic\n  // topic,\n  // 用户：bili_user\n  bili_user,\n  // 专栏：article\n  article,\n  // 相簿：photo\n  // photo\n}\n\nextension SearchTypeExtension on SearchType {\n  String get type =>\n      ['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];\n  String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];\n}\n\n// 搜索类型为视频时\nenum ArchiveFilterType {\n  totalrank,\n  click,\n  pubdate,\n  dm,\n  stow,\n  scores,\n  // 专栏\n  // attention,\n}\n\nextension ArchiveFilterTypeExtension on ArchiveFilterType {\n  String get description =>\n      ['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index];\n}\n\n// 搜索类型为专栏时\nenum ArticleFilterType {\n  // 综合排序\n  totalrank,\n  // 最新发布\n  pubdate,\n  // 最多点击\n  click,\n  // 最多喜欢\n  attention,\n  // 最多评论\n  scores,\n}\n\nextension ArticleFilterTypeExtension on ArticleFilterType {\n  String get description => ['综合排序', '最新发布', '最多点击', '最多喜欢', '最多评论'][index];\n}\n"
  },
  {
    "path": "lib/models/common/tab_type.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/pages/bangumi/index.dart';\nimport 'package:pilipala/pages/hot/index.dart';\nimport 'package:pilipala/pages/live/index.dart';\nimport 'package:pilipala/pages/rcmd/index.dart';\n\nenum TabType { live, rcmd, hot, bangumi }\n\nextension TabTypeDesc on TabType {\n  String get description => ['直播', '推荐', '热门', '番剧'][index];\n  String get id => ['live', 'rcmd', 'hot', 'bangumi'][index];\n}\n\nList tabsConfig = [\n  {\n    'icon': const Icon(\n      Icons.live_tv_outlined,\n      size: 15,\n    ),\n    'label': '直播',\n    'type': TabType.live,\n    'ctr': Get.find<LiveController>,\n    'page': const LivePage(),\n  },\n  {\n    'icon': const Icon(\n      Icons.thumb_up_off_alt_outlined,\n      size: 15,\n    ),\n    'label': '推荐',\n    'type': TabType.rcmd,\n    'ctr': Get.find<RcmdController>,\n    'page': const RcmdPage(),\n  },\n  {\n    'icon': const Icon(\n      Icons.whatshot_outlined,\n      size: 15,\n    ),\n    'label': '热门',\n    'type': TabType.hot,\n    'ctr': Get.find<HotController>,\n    'page': const HotPage(),\n  },\n  {\n    'icon': const Icon(\n      Icons.play_circle_outlined,\n      size: 15,\n    ),\n    'label': '番剧',\n    'type': TabType.bangumi,\n    'ctr': Get.find<BangumiController>,\n    'page': const BangumiPage(),\n  },\n];\n"
  },
  {
    "path": "lib/models/common/theme_type.dart",
    "content": "enum ThemeType {\n  light,\n  dark,\n  system,\n}\n\nextension ThemeTypeDesc on ThemeType {\n  String get description => ['浅色', '深色', '跟随系统'][index];\n}\n\nextension ThemeTypeCode on ThemeType {\n  int get code => [0, 1, 2][index];\n}\n"
  },
  {
    "path": "lib/models/common/video_episode_type.dart",
    "content": "enum VideoEpidoesType {\n  videoEpisode,\n  videoPart,\n  bangumiEpisode,\n}\n"
  },
  {
    "path": "lib/models/danmaku/dm.pb.dart",
    "content": "///\n//  Generated code. Do not modify.\n//  source: dm.proto\n//\n// @dart = 2.12\n// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name, no_leading_underscores_for_local_identifiers, depend_on_referenced_packages\n\nimport 'dart:async' as $async;\nimport 'dart:core' as $core;\n\nimport 'package:fixnum/fixnum.dart' as $fixnum;\nimport 'package:protobuf/protobuf.dart' as $pb;\n\nimport 'dm.pbenum.dart';\n\nexport 'dm.pbenum.dart';\n\nclass Avatar extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Avatar',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'id')\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'url')\n    ..e<AvatarType>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'avatarType',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: AvatarType.AvatarTypeNone,\n        valueOf: AvatarType.valueOf,\n        enumValues: AvatarType.values)\n    ..hasRequiredFields = false;\n\n  Avatar._() : super();\n  factory Avatar({\n    $core.String? id,\n    $core.String? url,\n    AvatarType? avatarType,\n  }) {\n    final _result = create();\n    if (id != null) {\n      _result.id = id;\n    }\n    if (url != null) {\n      _result.url = url;\n    }\n    if (avatarType != null) {\n      _result.avatarType = avatarType;\n    }\n    return _result;\n  }\n  factory Avatar.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Avatar.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Avatar clone() => Avatar()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Avatar copyWith(void Function(Avatar) updates) =>\n      super.copyWith((message) => updates(message as Avatar))\n          as Avatar; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Avatar create() => Avatar._();\n  Avatar createEmptyInstance() => create();\n  static $pb.PbList<Avatar> createRepeated() => $pb.PbList<Avatar>();\n  @$core.pragma('dart2js:noInline')\n  static Avatar getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Avatar>(create);\n  static Avatar? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get id => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set id($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasId() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearId() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get url => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set url($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasUrl() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearUrl() => clearField(2);\n\n  @$pb.TagNumber(3)\n  AvatarType get avatarType => $_getN(2);\n  @$pb.TagNumber(3)\n  set avatarType(AvatarType v) {\n    setField(3, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasAvatarType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearAvatarType() => clearField(3);\n}\n\nclass Bubble extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Bubble',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'url')\n    ..hasRequiredFields = false;\n\n  Bubble._() : super();\n  factory Bubble({\n    $core.String? text,\n    $core.String? url,\n  }) {\n    final _result = create();\n    if (text != null) {\n      _result.text = text;\n    }\n    if (url != null) {\n      _result.url = url;\n    }\n    return _result;\n  }\n  factory Bubble.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Bubble.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Bubble clone() => Bubble()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Bubble copyWith(void Function(Bubble) updates) =>\n      super.copyWith((message) => updates(message as Bubble))\n          as Bubble; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Bubble create() => Bubble._();\n  Bubble createEmptyInstance() => create();\n  static $pb.PbList<Bubble> createRepeated() => $pb.PbList<Bubble>();\n  @$core.pragma('dart2js:noInline')\n  static Bubble getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Bubble>(create);\n  static Bubble? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get text => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set text($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearText() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get url => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set url($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasUrl() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearUrl() => clearField(2);\n}\n\nclass BubbleV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'BubbleV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'url')\n    ..e<BubbleType>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'bubbleType',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: BubbleType.BubbleTypeNone,\n        valueOf: BubbleType.valueOf,\n        enumValues: BubbleType.values)\n    ..aOB(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'exposureOnce')\n    ..e<ExposureType>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'exposureType',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: ExposureType.ExposureTypeNone,\n        valueOf: ExposureType.valueOf,\n        enumValues: ExposureType.values)\n    ..hasRequiredFields = false;\n\n  BubbleV2._() : super();\n  factory BubbleV2({\n    $core.String? text,\n    $core.String? url,\n    BubbleType? bubbleType,\n    $core.bool? exposureOnce,\n    ExposureType? exposureType,\n  }) {\n    final _result = create();\n    if (text != null) {\n      _result.text = text;\n    }\n    if (url != null) {\n      _result.url = url;\n    }\n    if (bubbleType != null) {\n      _result.bubbleType = bubbleType;\n    }\n    if (exposureOnce != null) {\n      _result.exposureOnce = exposureOnce;\n    }\n    if (exposureType != null) {\n      _result.exposureType = exposureType;\n    }\n    return _result;\n  }\n  factory BubbleV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory BubbleV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  BubbleV2 clone() => BubbleV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  BubbleV2 copyWith(void Function(BubbleV2) updates) =>\n      super.copyWith((message) => updates(message as BubbleV2))\n          as BubbleV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static BubbleV2 create() => BubbleV2._();\n  BubbleV2 createEmptyInstance() => create();\n  static $pb.PbList<BubbleV2> createRepeated() => $pb.PbList<BubbleV2>();\n  @$core.pragma('dart2js:noInline')\n  static BubbleV2 getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BubbleV2>(create);\n  static BubbleV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get text => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set text($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearText() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get url => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set url($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasUrl() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearUrl() => clearField(2);\n\n  @$pb.TagNumber(3)\n  BubbleType get bubbleType => $_getN(2);\n  @$pb.TagNumber(3)\n  set bubbleType(BubbleType v) {\n    setField(3, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasBubbleType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearBubbleType() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.bool get exposureOnce => $_getBF(3);\n  @$pb.TagNumber(4)\n  set exposureOnce($core.bool v) {\n    $_setBool(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasExposureOnce() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearExposureOnce() => clearField(4);\n\n  @$pb.TagNumber(5)\n  ExposureType get exposureType => $_getN(4);\n  @$pb.TagNumber(5)\n  set exposureType(ExposureType v) {\n    setField(5, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasExposureType() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearExposureType() => clearField(5);\n}\n\nclass Button extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Button',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'action',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  Button._() : super();\n  factory Button({\n    $core.String? text,\n    $core.int? action,\n  }) {\n    final _result = create();\n    if (text != null) {\n      _result.text = text;\n    }\n    if (action != null) {\n      _result.action = action;\n    }\n    return _result;\n  }\n  factory Button.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Button.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Button clone() => Button()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Button copyWith(void Function(Button) updates) =>\n      super.copyWith((message) => updates(message as Button))\n          as Button; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Button create() => Button._();\n  Button createEmptyInstance() => create();\n  static $pb.PbList<Button> createRepeated() => $pb.PbList<Button>();\n  @$core.pragma('dart2js:noInline')\n  static Button getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Button>(create);\n  static Button? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get text => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set text($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearText() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.int get action => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set action($core.int v) {\n    $_setSignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasAction() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearAction() => clearField(2);\n}\n\nclass BuzzwordConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'BuzzwordConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pc<BuzzwordShowConfig>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'keywords',\n        $pb.PbFieldType.PM,\n        subBuilder: BuzzwordShowConfig.create)\n    ..hasRequiredFields = false;\n\n  BuzzwordConfig._() : super();\n  factory BuzzwordConfig({\n    $core.Iterable<BuzzwordShowConfig>? keywords,\n  }) {\n    final _result = create();\n    if (keywords != null) {\n      _result.keywords.addAll(keywords);\n    }\n    return _result;\n  }\n  factory BuzzwordConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory BuzzwordConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  BuzzwordConfig clone() => BuzzwordConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  BuzzwordConfig copyWith(void Function(BuzzwordConfig) updates) =>\n      super.copyWith((message) => updates(message as BuzzwordConfig))\n          as BuzzwordConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static BuzzwordConfig create() => BuzzwordConfig._();\n  BuzzwordConfig createEmptyInstance() => create();\n  static $pb.PbList<BuzzwordConfig> createRepeated() =>\n      $pb.PbList<BuzzwordConfig>();\n  @$core.pragma('dart2js:noInline')\n  static BuzzwordConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<BuzzwordConfig>(create);\n  static BuzzwordConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<BuzzwordShowConfig> get keywords => $_getList(0);\n}\n\nclass BuzzwordShowConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'BuzzwordShowConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'name')\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'schema')\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'source',\n        $pb.PbFieldType.O3)\n    ..aInt64(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'id')\n    ..aInt64(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'buzzwordId')\n    ..a<$core.int>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'schemaType',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  BuzzwordShowConfig._() : super();\n  factory BuzzwordShowConfig({\n    $core.String? name,\n    $core.String? schema,\n    $core.int? source,\n    $fixnum.Int64? id,\n    $fixnum.Int64? buzzwordId,\n    $core.int? schemaType,\n  }) {\n    final _result = create();\n    if (name != null) {\n      _result.name = name;\n    }\n    if (schema != null) {\n      _result.schema = schema;\n    }\n    if (source != null) {\n      _result.source = source;\n    }\n    if (id != null) {\n      _result.id = id;\n    }\n    if (buzzwordId != null) {\n      _result.buzzwordId = buzzwordId;\n    }\n    if (schemaType != null) {\n      _result.schemaType = schemaType;\n    }\n    return _result;\n  }\n  factory BuzzwordShowConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory BuzzwordShowConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  BuzzwordShowConfig clone() => BuzzwordShowConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  BuzzwordShowConfig copyWith(void Function(BuzzwordShowConfig) updates) =>\n      super.copyWith((message) => updates(message as BuzzwordShowConfig))\n          as BuzzwordShowConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static BuzzwordShowConfig create() => BuzzwordShowConfig._();\n  BuzzwordShowConfig createEmptyInstance() => create();\n  static $pb.PbList<BuzzwordShowConfig> createRepeated() =>\n      $pb.PbList<BuzzwordShowConfig>();\n  @$core.pragma('dart2js:noInline')\n  static BuzzwordShowConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<BuzzwordShowConfig>(create);\n  static BuzzwordShowConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get name => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set name($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasName() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearName() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get schema => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set schema($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasSchema() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearSchema() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get source => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set source($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasSource() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearSource() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $fixnum.Int64 get id => $_getI64(3);\n  @$pb.TagNumber(4)\n  set id($fixnum.Int64 v) {\n    $_setInt64(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasId() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearId() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $fixnum.Int64 get buzzwordId => $_getI64(4);\n  @$pb.TagNumber(5)\n  set buzzwordId($fixnum.Int64 v) {\n    $_setInt64(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasBuzzwordId() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearBuzzwordId() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.int get schemaType => $_getIZ(5);\n  @$pb.TagNumber(6)\n  set schemaType($core.int v) {\n    $_setSignedInt32(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasSchemaType() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearSchemaType() => clearField(6);\n}\n\nclass CheckBox extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'CheckBox',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..e<CheckboxType>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'type',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: CheckboxType.CheckboxTypeNone,\n        valueOf: CheckboxType.valueOf,\n        enumValues: CheckboxType.values)\n    ..aOB(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'defaultValue')\n    ..aOB(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'show')\n    ..hasRequiredFields = false;\n\n  CheckBox._() : super();\n  factory CheckBox({\n    $core.String? text,\n    CheckboxType? type,\n    $core.bool? defaultValue,\n    $core.bool? show,\n  }) {\n    final _result = create();\n    if (text != null) {\n      _result.text = text;\n    }\n    if (type != null) {\n      _result.type = type;\n    }\n    if (defaultValue != null) {\n      _result.defaultValue = defaultValue;\n    }\n    if (show != null) {\n      _result.show = show;\n    }\n    return _result;\n  }\n  factory CheckBox.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory CheckBox.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  CheckBox clone() => CheckBox()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  CheckBox copyWith(void Function(CheckBox) updates) =>\n      super.copyWith((message) => updates(message as CheckBox))\n          as CheckBox; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static CheckBox create() => CheckBox._();\n  CheckBox createEmptyInstance() => create();\n  static $pb.PbList<CheckBox> createRepeated() => $pb.PbList<CheckBox>();\n  @$core.pragma('dart2js:noInline')\n  static CheckBox getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CheckBox>(create);\n  static CheckBox? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get text => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set text($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearText() => clearField(1);\n\n  @$pb.TagNumber(2)\n  CheckboxType get type => $_getN(1);\n  @$pb.TagNumber(2)\n  set type(CheckboxType v) {\n    setField(2, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasType() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearType() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.bool get defaultValue => $_getBF(2);\n  @$pb.TagNumber(3)\n  set defaultValue($core.bool v) {\n    $_setBool(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasDefaultValue() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearDefaultValue() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.bool get show => $_getBF(3);\n  @$pb.TagNumber(4)\n  set show($core.bool v) {\n    $_setBool(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasShow() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearShow() => clearField(4);\n}\n\nclass CheckBoxV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'CheckBoxV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'type',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'defaultValue')\n    ..hasRequiredFields = false;\n\n  CheckBoxV2._() : super();\n  factory CheckBoxV2({\n    $core.String? text,\n    $core.int? type,\n    $core.bool? defaultValue,\n  }) {\n    final _result = create();\n    if (text != null) {\n      _result.text = text;\n    }\n    if (type != null) {\n      _result.type = type;\n    }\n    if (defaultValue != null) {\n      _result.defaultValue = defaultValue;\n    }\n    return _result;\n  }\n  factory CheckBoxV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory CheckBoxV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  CheckBoxV2 clone() => CheckBoxV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  CheckBoxV2 copyWith(void Function(CheckBoxV2) updates) =>\n      super.copyWith((message) => updates(message as CheckBoxV2))\n          as CheckBoxV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static CheckBoxV2 create() => CheckBoxV2._();\n  CheckBoxV2 createEmptyInstance() => create();\n  static $pb.PbList<CheckBoxV2> createRepeated() => $pb.PbList<CheckBoxV2>();\n  @$core.pragma('dart2js:noInline')\n  static CheckBoxV2 getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<CheckBoxV2>(create);\n  static CheckBoxV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get text => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set text($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearText() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.int get type => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set type($core.int v) {\n    $_setSignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasType() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearType() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.bool get defaultValue => $_getBF(2);\n  @$pb.TagNumber(3)\n  set defaultValue($core.bool v) {\n    $_setBool(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasDefaultValue() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearDefaultValue() => clearField(3);\n}\n\nclass ClickButton extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'ClickButton',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pPS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'portraitText')\n    ..pPS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'landscapeText')\n    ..pPS(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'portraitTextFocus')\n    ..pPS(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'landscapeTextFocus')\n    ..e<RenderType>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'renderType',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: RenderType.RenderTypeNone,\n        valueOf: RenderType.valueOf,\n        enumValues: RenderType.values)\n    ..aOB(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'show')\n    ..aOM<Bubble>(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'bubble',\n        subBuilder: Bubble.create)\n    ..hasRequiredFields = false;\n\n  ClickButton._() : super();\n  factory ClickButton({\n    $core.Iterable<$core.String>? portraitText,\n    $core.Iterable<$core.String>? landscapeText,\n    $core.Iterable<$core.String>? portraitTextFocus,\n    $core.Iterable<$core.String>? landscapeTextFocus,\n    RenderType? renderType,\n    $core.bool? show,\n    Bubble? bubble,\n  }) {\n    final _result = create();\n    if (portraitText != null) {\n      _result.portraitText.addAll(portraitText);\n    }\n    if (landscapeText != null) {\n      _result.landscapeText.addAll(landscapeText);\n    }\n    if (portraitTextFocus != null) {\n      _result.portraitTextFocus.addAll(portraitTextFocus);\n    }\n    if (landscapeTextFocus != null) {\n      _result.landscapeTextFocus.addAll(landscapeTextFocus);\n    }\n    if (renderType != null) {\n      _result.renderType = renderType;\n    }\n    if (show != null) {\n      _result.show = show;\n    }\n    if (bubble != null) {\n      _result.bubble = bubble;\n    }\n    return _result;\n  }\n  factory ClickButton.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory ClickButton.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  ClickButton clone() => ClickButton()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  ClickButton copyWith(void Function(ClickButton) updates) =>\n      super.copyWith((message) => updates(message as ClickButton))\n          as ClickButton; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static ClickButton create() => ClickButton._();\n  ClickButton createEmptyInstance() => create();\n  static $pb.PbList<ClickButton> createRepeated() => $pb.PbList<ClickButton>();\n  @$core.pragma('dart2js:noInline')\n  static ClickButton getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<ClickButton>(create);\n  static ClickButton? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<$core.String> get portraitText => $_getList(0);\n\n  @$pb.TagNumber(2)\n  $core.List<$core.String> get landscapeText => $_getList(1);\n\n  @$pb.TagNumber(3)\n  $core.List<$core.String> get portraitTextFocus => $_getList(2);\n\n  @$pb.TagNumber(4)\n  $core.List<$core.String> get landscapeTextFocus => $_getList(3);\n\n  @$pb.TagNumber(5)\n  RenderType get renderType => $_getN(4);\n  @$pb.TagNumber(5)\n  set renderType(RenderType v) {\n    setField(5, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasRenderType() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearRenderType() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.bool get show => $_getBF(5);\n  @$pb.TagNumber(6)\n  set show($core.bool v) {\n    $_setBool(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasShow() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearShow() => clearField(6);\n\n  @$pb.TagNumber(7)\n  Bubble get bubble => $_getN(6);\n  @$pb.TagNumber(7)\n  set bubble(Bubble v) {\n    setField(7, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasBubble() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearBubble() => clearField(7);\n  @$pb.TagNumber(7)\n  Bubble ensureBubble() => $_ensure(6);\n}\n\nclass ClickButtonV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'ClickButtonV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pPS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'portraitText')\n    ..pPS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'landscapeText')\n    ..pPS(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'portraitTextFocus')\n    ..pPS(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'landscapeTextFocus')\n    ..a<$core.int>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'renderType',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'textInputPost')\n    ..aOB(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'exposureOnce')\n    ..a<$core.int>(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'exposureType',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  ClickButtonV2._() : super();\n  factory ClickButtonV2({\n    $core.Iterable<$core.String>? portraitText,\n    $core.Iterable<$core.String>? landscapeText,\n    $core.Iterable<$core.String>? portraitTextFocus,\n    $core.Iterable<$core.String>? landscapeTextFocus,\n    $core.int? renderType,\n    $core.bool? textInputPost,\n    $core.bool? exposureOnce,\n    $core.int? exposureType,\n  }) {\n    final _result = create();\n    if (portraitText != null) {\n      _result.portraitText.addAll(portraitText);\n    }\n    if (landscapeText != null) {\n      _result.landscapeText.addAll(landscapeText);\n    }\n    if (portraitTextFocus != null) {\n      _result.portraitTextFocus.addAll(portraitTextFocus);\n    }\n    if (landscapeTextFocus != null) {\n      _result.landscapeTextFocus.addAll(landscapeTextFocus);\n    }\n    if (renderType != null) {\n      _result.renderType = renderType;\n    }\n    if (textInputPost != null) {\n      _result.textInputPost = textInputPost;\n    }\n    if (exposureOnce != null) {\n      _result.exposureOnce = exposureOnce;\n    }\n    if (exposureType != null) {\n      _result.exposureType = exposureType;\n    }\n    return _result;\n  }\n  factory ClickButtonV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory ClickButtonV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  ClickButtonV2 clone() => ClickButtonV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  ClickButtonV2 copyWith(void Function(ClickButtonV2) updates) =>\n      super.copyWith((message) => updates(message as ClickButtonV2))\n          as ClickButtonV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static ClickButtonV2 create() => ClickButtonV2._();\n  ClickButtonV2 createEmptyInstance() => create();\n  static $pb.PbList<ClickButtonV2> createRepeated() =>\n      $pb.PbList<ClickButtonV2>();\n  @$core.pragma('dart2js:noInline')\n  static ClickButtonV2 getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<ClickButtonV2>(create);\n  static ClickButtonV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<$core.String> get portraitText => $_getList(0);\n\n  @$pb.TagNumber(2)\n  $core.List<$core.String> get landscapeText => $_getList(1);\n\n  @$pb.TagNumber(3)\n  $core.List<$core.String> get portraitTextFocus => $_getList(2);\n\n  @$pb.TagNumber(4)\n  $core.List<$core.String> get landscapeTextFocus => $_getList(3);\n\n  @$pb.TagNumber(5)\n  $core.int get renderType => $_getIZ(4);\n  @$pb.TagNumber(5)\n  set renderType($core.int v) {\n    $_setSignedInt32(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasRenderType() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearRenderType() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.bool get textInputPost => $_getBF(5);\n  @$pb.TagNumber(6)\n  set textInputPost($core.bool v) {\n    $_setBool(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasTextInputPost() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearTextInputPost() => clearField(6);\n\n  @$pb.TagNumber(7)\n  $core.bool get exposureOnce => $_getBF(6);\n  @$pb.TagNumber(7)\n  set exposureOnce($core.bool v) {\n    $_setBool(6, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasExposureOnce() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearExposureOnce() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $core.int get exposureType => $_getIZ(7);\n  @$pb.TagNumber(8)\n  set exposureType($core.int v) {\n    $_setSignedInt32(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasExposureType() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearExposureType() => clearField(8);\n}\n\nclass CommandDm extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'CommandDm',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'id')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'oid')\n    ..aOS(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'mid')\n    ..aOS(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'command')\n    ..aOS(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'content')\n    ..a<$core.int>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'progress',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'ctime')\n    ..aOS(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'mtime')\n    ..aOS(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'extra')\n    ..aOS(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'idStr',\n        protoName: 'idStr')\n    ..hasRequiredFields = false;\n\n  CommandDm._() : super();\n  factory CommandDm({\n    $fixnum.Int64? id,\n    $fixnum.Int64? oid,\n    $core.String? mid,\n    $core.String? command,\n    $core.String? content,\n    $core.int? progress,\n    $core.String? ctime,\n    $core.String? mtime,\n    $core.String? extra,\n    $core.String? idStr,\n  }) {\n    final _result = create();\n    if (id != null) {\n      _result.id = id;\n    }\n    if (oid != null) {\n      _result.oid = oid;\n    }\n    if (mid != null) {\n      _result.mid = mid;\n    }\n    if (command != null) {\n      _result.command = command;\n    }\n    if (content != null) {\n      _result.content = content;\n    }\n    if (progress != null) {\n      _result.progress = progress;\n    }\n    if (ctime != null) {\n      _result.ctime = ctime;\n    }\n    if (mtime != null) {\n      _result.mtime = mtime;\n    }\n    if (extra != null) {\n      _result.extra = extra;\n    }\n    if (idStr != null) {\n      _result.idStr = idStr;\n    }\n    return _result;\n  }\n  factory CommandDm.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory CommandDm.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  CommandDm clone() => CommandDm()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  CommandDm copyWith(void Function(CommandDm) updates) =>\n      super.copyWith((message) => updates(message as CommandDm))\n          as CommandDm; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static CommandDm create() => CommandDm._();\n  CommandDm createEmptyInstance() => create();\n  static $pb.PbList<CommandDm> createRepeated() => $pb.PbList<CommandDm>();\n  @$core.pragma('dart2js:noInline')\n  static CommandDm getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CommandDm>(create);\n  static CommandDm? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get id => $_getI64(0);\n  @$pb.TagNumber(1)\n  set id($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasId() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearId() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get oid => $_getI64(1);\n  @$pb.TagNumber(2)\n  set oid($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasOid() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearOid() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.String get mid => $_getSZ(2);\n  @$pb.TagNumber(3)\n  set mid($core.String v) {\n    $_setString(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasMid() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearMid() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.String get command => $_getSZ(3);\n  @$pb.TagNumber(4)\n  set command($core.String v) {\n    $_setString(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasCommand() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearCommand() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.String get content => $_getSZ(4);\n  @$pb.TagNumber(5)\n  set content($core.String v) {\n    $_setString(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasContent() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearContent() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.int get progress => $_getIZ(5);\n  @$pb.TagNumber(6)\n  set progress($core.int v) {\n    $_setSignedInt32(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasProgress() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearProgress() => clearField(6);\n\n  @$pb.TagNumber(7)\n  $core.String get ctime => $_getSZ(6);\n  @$pb.TagNumber(7)\n  set ctime($core.String v) {\n    $_setString(6, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasCtime() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearCtime() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $core.String get mtime => $_getSZ(7);\n  @$pb.TagNumber(8)\n  set mtime($core.String v) {\n    $_setString(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasMtime() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearMtime() => clearField(8);\n\n  @$pb.TagNumber(9)\n  $core.String get extra => $_getSZ(8);\n  @$pb.TagNumber(9)\n  set extra($core.String v) {\n    $_setString(8, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasExtra() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearExtra() => clearField(9);\n\n  @$pb.TagNumber(10)\n  $core.String get idStr => $_getSZ(9);\n  @$pb.TagNumber(10)\n  set idStr($core.String v) {\n    $_setString(9, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasIdStr() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearIdStr() => clearField(10);\n}\n\nclass DanmakuAIFlag extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmakuAIFlag',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pc<DanmakuFlag>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'dmFlags',\n        $pb.PbFieldType.PM,\n        subBuilder: DanmakuFlag.create)\n    ..hasRequiredFields = false;\n\n  DanmakuAIFlag._() : super();\n  factory DanmakuAIFlag({\n    $core.Iterable<DanmakuFlag>? dmFlags,\n  }) {\n    final _result = create();\n    if (dmFlags != null) {\n      _result.dmFlags.addAll(dmFlags);\n    }\n    return _result;\n  }\n  factory DanmakuAIFlag.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmakuAIFlag.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmakuAIFlag clone() => DanmakuAIFlag()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmakuAIFlag copyWith(void Function(DanmakuAIFlag) updates) =>\n      super.copyWith((message) => updates(message as DanmakuAIFlag))\n          as DanmakuAIFlag; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmakuAIFlag create() => DanmakuAIFlag._();\n  DanmakuAIFlag createEmptyInstance() => create();\n  static $pb.PbList<DanmakuAIFlag> createRepeated() =>\n      $pb.PbList<DanmakuAIFlag>();\n  @$core.pragma('dart2js:noInline')\n  static DanmakuAIFlag getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmakuAIFlag>(create);\n  static DanmakuAIFlag? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<DanmakuFlag> get dmFlags => $_getList(0);\n}\n\nclass DanmakuElem extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmakuElem',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'id')\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'progress',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'mode',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'fontsize',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'color',\n        $pb.PbFieldType.OU3)\n    ..aOS(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'midHash',\n        protoName: 'midHash')\n    ..aOS(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'content')\n    ..aInt64(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'ctime')\n    ..a<$core.int>(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'weight',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'action')\n    ..a<$core.int>(\n        11,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'pool',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        12,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'idStr',\n        protoName: 'idStr')\n    ..a<$core.int>(\n        13,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'attr',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        22,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'animation')\n    ..hasRequiredFields = false;\n\n  DanmakuElem._() : super();\n  factory DanmakuElem({\n    $fixnum.Int64? id,\n    $core.int? progress,\n    $core.int? mode,\n    $core.int? fontsize,\n    $core.int? color,\n    $core.String? midHash,\n    $core.String? content,\n    $fixnum.Int64? ctime,\n    $core.int? weight,\n    $core.String? action,\n    $core.int? pool,\n    $core.String? idStr,\n    $core.int? attr,\n    $core.String? animation,\n  }) {\n    final _result = create();\n    if (id != null) {\n      _result.id = id;\n    }\n    if (progress != null) {\n      _result.progress = progress;\n    }\n    if (mode != null) {\n      _result.mode = mode;\n    }\n    if (fontsize != null) {\n      _result.fontsize = fontsize;\n    }\n    if (color != null) {\n      _result.color = color;\n    }\n    if (midHash != null) {\n      _result.midHash = midHash;\n    }\n    if (content != null) {\n      _result.content = content;\n    }\n    if (ctime != null) {\n      _result.ctime = ctime;\n    }\n    if (weight != null) {\n      _result.weight = weight;\n    }\n    if (action != null) {\n      _result.action = action;\n    }\n    if (pool != null) {\n      _result.pool = pool;\n    }\n    if (idStr != null) {\n      _result.idStr = idStr;\n    }\n    if (attr != null) {\n      _result.attr = attr;\n    }\n    if (animation != null) {\n      _result.animation = animation;\n    }\n    return _result;\n  }\n  factory DanmakuElem.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmakuElem.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmakuElem clone() => DanmakuElem()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmakuElem copyWith(void Function(DanmakuElem) updates) =>\n      super.copyWith((message) => updates(message as DanmakuElem))\n          as DanmakuElem; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmakuElem create() => DanmakuElem._();\n  DanmakuElem createEmptyInstance() => create();\n  static $pb.PbList<DanmakuElem> createRepeated() => $pb.PbList<DanmakuElem>();\n  @$core.pragma('dart2js:noInline')\n  static DanmakuElem getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmakuElem>(create);\n  static DanmakuElem? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get id => $_getI64(0);\n  @$pb.TagNumber(1)\n  set id($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasId() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearId() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.int get progress => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set progress($core.int v) {\n    $_setSignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasProgress() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearProgress() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get mode => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set mode($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasMode() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearMode() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.int get fontsize => $_getIZ(3);\n  @$pb.TagNumber(4)\n  set fontsize($core.int v) {\n    $_setSignedInt32(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasFontsize() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearFontsize() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.int get color => $_getIZ(4);\n  @$pb.TagNumber(5)\n  set color($core.int v) {\n    $_setUnsignedInt32(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasColor() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearColor() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.String get midHash => $_getSZ(5);\n  @$pb.TagNumber(6)\n  set midHash($core.String v) {\n    $_setString(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasMidHash() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearMidHash() => clearField(6);\n\n  @$pb.TagNumber(7)\n  $core.String get content => $_getSZ(6);\n  @$pb.TagNumber(7)\n  set content($core.String v) {\n    $_setString(6, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasContent() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearContent() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $fixnum.Int64 get ctime => $_getI64(7);\n  @$pb.TagNumber(8)\n  set ctime($fixnum.Int64 v) {\n    $_setInt64(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasCtime() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearCtime() => clearField(8);\n\n  @$pb.TagNumber(9)\n  $core.int get weight => $_getIZ(8);\n  @$pb.TagNumber(9)\n  set weight($core.int v) {\n    $_setSignedInt32(8, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasWeight() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearWeight() => clearField(9);\n\n  @$pb.TagNumber(10)\n  $core.String get action => $_getSZ(9);\n  @$pb.TagNumber(10)\n  set action($core.String v) {\n    $_setString(9, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasAction() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearAction() => clearField(10);\n\n  @$pb.TagNumber(11)\n  $core.int get pool => $_getIZ(10);\n  @$pb.TagNumber(11)\n  set pool($core.int v) {\n    $_setSignedInt32(10, v);\n  }\n\n  @$pb.TagNumber(11)\n  $core.bool hasPool() => $_has(10);\n  @$pb.TagNumber(11)\n  void clearPool() => clearField(11);\n\n  @$pb.TagNumber(12)\n  $core.String get idStr => $_getSZ(11);\n  @$pb.TagNumber(12)\n  set idStr($core.String v) {\n    $_setString(11, v);\n  }\n\n  @$pb.TagNumber(12)\n  $core.bool hasIdStr() => $_has(11);\n  @$pb.TagNumber(12)\n  void clearIdStr() => clearField(12);\n\n  @$pb.TagNumber(13)\n  $core.int get attr => $_getIZ(12);\n  @$pb.TagNumber(13)\n  set attr($core.int v) {\n    $_setSignedInt32(12, v);\n  }\n\n  @$pb.TagNumber(13)\n  $core.bool hasAttr() => $_has(12);\n  @$pb.TagNumber(13)\n  void clearAttr() => clearField(13);\n\n  @$pb.TagNumber(22)\n  $core.String get animation => $_getSZ(13);\n  @$pb.TagNumber(22)\n  set animation($core.String v) {\n    $_setString(13, v);\n  }\n\n  @$pb.TagNumber(22)\n  $core.bool hasAnimation() => $_has(13);\n  @$pb.TagNumber(22)\n  void clearAnimation() => clearField(22);\n}\n\nclass DanmakuFlag extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmakuFlag',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'dmid')\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'flag',\n        $pb.PbFieldType.OU3)\n    ..hasRequiredFields = false;\n\n  DanmakuFlag._() : super();\n  factory DanmakuFlag({\n    $fixnum.Int64? dmid,\n    $core.int? flag,\n  }) {\n    final _result = create();\n    if (dmid != null) {\n      _result.dmid = dmid;\n    }\n    if (flag != null) {\n      _result.flag = flag;\n    }\n    return _result;\n  }\n  factory DanmakuFlag.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmakuFlag.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmakuFlag clone() => DanmakuFlag()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmakuFlag copyWith(void Function(DanmakuFlag) updates) =>\n      super.copyWith((message) => updates(message as DanmakuFlag))\n          as DanmakuFlag; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmakuFlag create() => DanmakuFlag._();\n  DanmakuFlag createEmptyInstance() => create();\n  static $pb.PbList<DanmakuFlag> createRepeated() => $pb.PbList<DanmakuFlag>();\n  @$core.pragma('dart2js:noInline')\n  static DanmakuFlag getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmakuFlag>(create);\n  static DanmakuFlag? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get dmid => $_getI64(0);\n  @$pb.TagNumber(1)\n  set dmid($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasDmid() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearDmid() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.int get flag => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set flag($core.int v) {\n    $_setUnsignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasFlag() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearFlag() => clearField(2);\n}\n\nclass DanmakuFlagConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmakuFlagConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.int>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'recFlag',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'recText')\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'recSwitch',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  DanmakuFlagConfig._() : super();\n  factory DanmakuFlagConfig({\n    $core.int? recFlag,\n    $core.String? recText,\n    $core.int? recSwitch,\n  }) {\n    final _result = create();\n    if (recFlag != null) {\n      _result.recFlag = recFlag;\n    }\n    if (recText != null) {\n      _result.recText = recText;\n    }\n    if (recSwitch != null) {\n      _result.recSwitch = recSwitch;\n    }\n    return _result;\n  }\n  factory DanmakuFlagConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmakuFlagConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmakuFlagConfig clone() => DanmakuFlagConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmakuFlagConfig copyWith(void Function(DanmakuFlagConfig) updates) =>\n      super.copyWith((message) => updates(message as DanmakuFlagConfig))\n          as DanmakuFlagConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmakuFlagConfig create() => DanmakuFlagConfig._();\n  DanmakuFlagConfig createEmptyInstance() => create();\n  static $pb.PbList<DanmakuFlagConfig> createRepeated() =>\n      $pb.PbList<DanmakuFlagConfig>();\n  @$core.pragma('dart2js:noInline')\n  static DanmakuFlagConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmakuFlagConfig>(create);\n  static DanmakuFlagConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.int get recFlag => $_getIZ(0);\n  @$pb.TagNumber(1)\n  set recFlag($core.int v) {\n    $_setSignedInt32(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasRecFlag() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearRecFlag() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get recText => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set recText($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasRecText() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearRecText() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get recSwitch => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set recSwitch($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasRecSwitch() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearRecSwitch() => clearField(3);\n}\n\nclass DanmuDefaultPlayerConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmuDefaultPlayerConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuUseDefaultConfig')\n    ..aOB(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuAiRecommendedSwitch')\n    ..a<$core.int>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuAiRecommendedLevel',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlocktop')\n    ..aOB(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockscroll')\n    ..aOB(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockbottom')\n    ..aOB(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockcolorful')\n    ..aOB(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockrepeat')\n    ..aOB(\n        11,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockspecial')\n    ..a<$core.double>(\n        12,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuOpacity',\n        $pb.PbFieldType.OF)\n    ..a<$core.double>(\n        13,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuScalingfactor',\n        $pb.PbFieldType.OF)\n    ..a<$core.double>(\n        14,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuDomain',\n        $pb.PbFieldType.OF)\n    ..a<$core.int>(\n        15,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuSpeed',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        16,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'inlinePlayerDanmakuSwitch')\n    ..a<$core.int>(\n        17,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuSeniorModeSwitch',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        18,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuAiRecommendedLevelV2',\n        $pb.PbFieldType.O3)\n    ..m<$core.int, $core.int>(\n        19,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuAiRecommendedLevelV2Map',\n        entryClassName:\n            'DanmuDefaultPlayerConfig.PlayerDanmakuAiRecommendedLevelV2MapEntry',\n        keyFieldType: $pb.PbFieldType.O3,\n        valueFieldType: $pb.PbFieldType.O3,\n        packageName: const $pb.PackageName('bilibili.community.service.dm.v1'))\n    ..hasRequiredFields = false;\n\n  DanmuDefaultPlayerConfig._() : super();\n  factory DanmuDefaultPlayerConfig({\n    $core.bool? playerDanmakuUseDefaultConfig,\n    $core.bool? playerDanmakuAiRecommendedSwitch,\n    $core.int? playerDanmakuAiRecommendedLevel,\n    $core.bool? playerDanmakuBlocktop,\n    $core.bool? playerDanmakuBlockscroll,\n    $core.bool? playerDanmakuBlockbottom,\n    $core.bool? playerDanmakuBlockcolorful,\n    $core.bool? playerDanmakuBlockrepeat,\n    $core.bool? playerDanmakuBlockspecial,\n    $core.double? playerDanmakuOpacity,\n    $core.double? playerDanmakuScalingfactor,\n    $core.double? playerDanmakuDomain,\n    $core.int? playerDanmakuSpeed,\n    $core.bool? inlinePlayerDanmakuSwitch,\n    $core.int? playerDanmakuSeniorModeSwitch,\n    $core.int? playerDanmakuAiRecommendedLevelV2,\n    $core.Map<$core.int, $core.int>? playerDanmakuAiRecommendedLevelV2Map,\n  }) {\n    final _result = create();\n    if (playerDanmakuUseDefaultConfig != null) {\n      _result.playerDanmakuUseDefaultConfig = playerDanmakuUseDefaultConfig;\n    }\n    if (playerDanmakuAiRecommendedSwitch != null) {\n      _result.playerDanmakuAiRecommendedSwitch =\n          playerDanmakuAiRecommendedSwitch;\n    }\n    if (playerDanmakuAiRecommendedLevel != null) {\n      _result.playerDanmakuAiRecommendedLevel = playerDanmakuAiRecommendedLevel;\n    }\n    if (playerDanmakuBlocktop != null) {\n      _result.playerDanmakuBlocktop = playerDanmakuBlocktop;\n    }\n    if (playerDanmakuBlockscroll != null) {\n      _result.playerDanmakuBlockscroll = playerDanmakuBlockscroll;\n    }\n    if (playerDanmakuBlockbottom != null) {\n      _result.playerDanmakuBlockbottom = playerDanmakuBlockbottom;\n    }\n    if (playerDanmakuBlockcolorful != null) {\n      _result.playerDanmakuBlockcolorful = playerDanmakuBlockcolorful;\n    }\n    if (playerDanmakuBlockrepeat != null) {\n      _result.playerDanmakuBlockrepeat = playerDanmakuBlockrepeat;\n    }\n    if (playerDanmakuBlockspecial != null) {\n      _result.playerDanmakuBlockspecial = playerDanmakuBlockspecial;\n    }\n    if (playerDanmakuOpacity != null) {\n      _result.playerDanmakuOpacity = playerDanmakuOpacity;\n    }\n    if (playerDanmakuScalingfactor != null) {\n      _result.playerDanmakuScalingfactor = playerDanmakuScalingfactor;\n    }\n    if (playerDanmakuDomain != null) {\n      _result.playerDanmakuDomain = playerDanmakuDomain;\n    }\n    if (playerDanmakuSpeed != null) {\n      _result.playerDanmakuSpeed = playerDanmakuSpeed;\n    }\n    if (inlinePlayerDanmakuSwitch != null) {\n      _result.inlinePlayerDanmakuSwitch = inlinePlayerDanmakuSwitch;\n    }\n    if (playerDanmakuSeniorModeSwitch != null) {\n      _result.playerDanmakuSeniorModeSwitch = playerDanmakuSeniorModeSwitch;\n    }\n    if (playerDanmakuAiRecommendedLevelV2 != null) {\n      _result.playerDanmakuAiRecommendedLevelV2 =\n          playerDanmakuAiRecommendedLevelV2;\n    }\n    if (playerDanmakuAiRecommendedLevelV2Map != null) {\n      _result.playerDanmakuAiRecommendedLevelV2Map\n          .addAll(playerDanmakuAiRecommendedLevelV2Map);\n    }\n    return _result;\n  }\n  factory DanmuDefaultPlayerConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmuDefaultPlayerConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmuDefaultPlayerConfig clone() =>\n      DanmuDefaultPlayerConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmuDefaultPlayerConfig copyWith(\n          void Function(DanmuDefaultPlayerConfig) updates) =>\n      super.copyWith((message) => updates(message as DanmuDefaultPlayerConfig))\n          as DanmuDefaultPlayerConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmuDefaultPlayerConfig create() => DanmuDefaultPlayerConfig._();\n  DanmuDefaultPlayerConfig createEmptyInstance() => create();\n  static $pb.PbList<DanmuDefaultPlayerConfig> createRepeated() =>\n      $pb.PbList<DanmuDefaultPlayerConfig>();\n  @$core.pragma('dart2js:noInline')\n  static DanmuDefaultPlayerConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmuDefaultPlayerConfig>(create);\n  static DanmuDefaultPlayerConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get playerDanmakuUseDefaultConfig => $_getBF(0);\n  @$pb.TagNumber(1)\n  set playerDanmakuUseDefaultConfig($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasPlayerDanmakuUseDefaultConfig() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearPlayerDanmakuUseDefaultConfig() => clearField(1);\n\n  @$pb.TagNumber(4)\n  $core.bool get playerDanmakuAiRecommendedSwitch => $_getBF(1);\n  @$pb.TagNumber(4)\n  set playerDanmakuAiRecommendedSwitch($core.bool v) {\n    $_setBool(1, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasPlayerDanmakuAiRecommendedSwitch() => $_has(1);\n  @$pb.TagNumber(4)\n  void clearPlayerDanmakuAiRecommendedSwitch() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.int get playerDanmakuAiRecommendedLevel => $_getIZ(2);\n  @$pb.TagNumber(5)\n  set playerDanmakuAiRecommendedLevel($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasPlayerDanmakuAiRecommendedLevel() => $_has(2);\n  @$pb.TagNumber(5)\n  void clearPlayerDanmakuAiRecommendedLevel() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.bool get playerDanmakuBlocktop => $_getBF(3);\n  @$pb.TagNumber(6)\n  set playerDanmakuBlocktop($core.bool v) {\n    $_setBool(3, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasPlayerDanmakuBlocktop() => $_has(3);\n  @$pb.TagNumber(6)\n  void clearPlayerDanmakuBlocktop() => clearField(6);\n\n  @$pb.TagNumber(7)\n  $core.bool get playerDanmakuBlockscroll => $_getBF(4);\n  @$pb.TagNumber(7)\n  set playerDanmakuBlockscroll($core.bool v) {\n    $_setBool(4, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasPlayerDanmakuBlockscroll() => $_has(4);\n  @$pb.TagNumber(7)\n  void clearPlayerDanmakuBlockscroll() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $core.bool get playerDanmakuBlockbottom => $_getBF(5);\n  @$pb.TagNumber(8)\n  set playerDanmakuBlockbottom($core.bool v) {\n    $_setBool(5, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasPlayerDanmakuBlockbottom() => $_has(5);\n  @$pb.TagNumber(8)\n  void clearPlayerDanmakuBlockbottom() => clearField(8);\n\n  @$pb.TagNumber(9)\n  $core.bool get playerDanmakuBlockcolorful => $_getBF(6);\n  @$pb.TagNumber(9)\n  set playerDanmakuBlockcolorful($core.bool v) {\n    $_setBool(6, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasPlayerDanmakuBlockcolorful() => $_has(6);\n  @$pb.TagNumber(9)\n  void clearPlayerDanmakuBlockcolorful() => clearField(9);\n\n  @$pb.TagNumber(10)\n  $core.bool get playerDanmakuBlockrepeat => $_getBF(7);\n  @$pb.TagNumber(10)\n  set playerDanmakuBlockrepeat($core.bool v) {\n    $_setBool(7, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasPlayerDanmakuBlockrepeat() => $_has(7);\n  @$pb.TagNumber(10)\n  void clearPlayerDanmakuBlockrepeat() => clearField(10);\n\n  @$pb.TagNumber(11)\n  $core.bool get playerDanmakuBlockspecial => $_getBF(8);\n  @$pb.TagNumber(11)\n  set playerDanmakuBlockspecial($core.bool v) {\n    $_setBool(8, v);\n  }\n\n  @$pb.TagNumber(11)\n  $core.bool hasPlayerDanmakuBlockspecial() => $_has(8);\n  @$pb.TagNumber(11)\n  void clearPlayerDanmakuBlockspecial() => clearField(11);\n\n  @$pb.TagNumber(12)\n  $core.double get playerDanmakuOpacity => $_getN(9);\n  @$pb.TagNumber(12)\n  set playerDanmakuOpacity($core.double v) {\n    $_setFloat(9, v);\n  }\n\n  @$pb.TagNumber(12)\n  $core.bool hasPlayerDanmakuOpacity() => $_has(9);\n  @$pb.TagNumber(12)\n  void clearPlayerDanmakuOpacity() => clearField(12);\n\n  @$pb.TagNumber(13)\n  $core.double get playerDanmakuScalingfactor => $_getN(10);\n  @$pb.TagNumber(13)\n  set playerDanmakuScalingfactor($core.double v) {\n    $_setFloat(10, v);\n  }\n\n  @$pb.TagNumber(13)\n  $core.bool hasPlayerDanmakuScalingfactor() => $_has(10);\n  @$pb.TagNumber(13)\n  void clearPlayerDanmakuScalingfactor() => clearField(13);\n\n  @$pb.TagNumber(14)\n  $core.double get playerDanmakuDomain => $_getN(11);\n  @$pb.TagNumber(14)\n  set playerDanmakuDomain($core.double v) {\n    $_setFloat(11, v);\n  }\n\n  @$pb.TagNumber(14)\n  $core.bool hasPlayerDanmakuDomain() => $_has(11);\n  @$pb.TagNumber(14)\n  void clearPlayerDanmakuDomain() => clearField(14);\n\n  @$pb.TagNumber(15)\n  $core.int get playerDanmakuSpeed => $_getIZ(12);\n  @$pb.TagNumber(15)\n  set playerDanmakuSpeed($core.int v) {\n    $_setSignedInt32(12, v);\n  }\n\n  @$pb.TagNumber(15)\n  $core.bool hasPlayerDanmakuSpeed() => $_has(12);\n  @$pb.TagNumber(15)\n  void clearPlayerDanmakuSpeed() => clearField(15);\n\n  @$pb.TagNumber(16)\n  $core.bool get inlinePlayerDanmakuSwitch => $_getBF(13);\n  @$pb.TagNumber(16)\n  set inlinePlayerDanmakuSwitch($core.bool v) {\n    $_setBool(13, v);\n  }\n\n  @$pb.TagNumber(16)\n  $core.bool hasInlinePlayerDanmakuSwitch() => $_has(13);\n  @$pb.TagNumber(16)\n  void clearInlinePlayerDanmakuSwitch() => clearField(16);\n\n  @$pb.TagNumber(17)\n  $core.int get playerDanmakuSeniorModeSwitch => $_getIZ(14);\n  @$pb.TagNumber(17)\n  set playerDanmakuSeniorModeSwitch($core.int v) {\n    $_setSignedInt32(14, v);\n  }\n\n  @$pb.TagNumber(17)\n  $core.bool hasPlayerDanmakuSeniorModeSwitch() => $_has(14);\n  @$pb.TagNumber(17)\n  void clearPlayerDanmakuSeniorModeSwitch() => clearField(17);\n\n  @$pb.TagNumber(18)\n  $core.int get playerDanmakuAiRecommendedLevelV2 => $_getIZ(15);\n  @$pb.TagNumber(18)\n  set playerDanmakuAiRecommendedLevelV2($core.int v) {\n    $_setSignedInt32(15, v);\n  }\n\n  @$pb.TagNumber(18)\n  $core.bool hasPlayerDanmakuAiRecommendedLevelV2() => $_has(15);\n  @$pb.TagNumber(18)\n  void clearPlayerDanmakuAiRecommendedLevelV2() => clearField(18);\n\n  @$pb.TagNumber(19)\n  $core.Map<$core.int, $core.int> get playerDanmakuAiRecommendedLevelV2Map =>\n      $_getMap(16);\n}\n\nclass DanmuPlayerConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmuPlayerConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuSwitch')\n    ..aOB(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuSwitchSave')\n    ..aOB(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuUseDefaultConfig')\n    ..aOB(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuAiRecommendedSwitch')\n    ..a<$core.int>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuAiRecommendedLevel',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlocktop')\n    ..aOB(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockscroll')\n    ..aOB(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockbottom')\n    ..aOB(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockcolorful')\n    ..aOB(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockrepeat')\n    ..aOB(\n        11,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuBlockspecial')\n    ..a<$core.double>(\n        12,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuOpacity',\n        $pb.PbFieldType.OF)\n    ..a<$core.double>(\n        13,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuScalingfactor',\n        $pb.PbFieldType.OF)\n    ..a<$core.double>(\n        14,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuDomain',\n        $pb.PbFieldType.OF)\n    ..a<$core.int>(\n        15,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuSpeed',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        16,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuEnableblocklist')\n    ..aOB(\n        17,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'inlinePlayerDanmakuSwitch')\n    ..a<$core.int>(\n        18,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'inlinePlayerDanmakuConfig',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        19,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuIosSwitchSave',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        20,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuSeniorModeSwitch',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        21,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuAiRecommendedLevelV2',\n        $pb.PbFieldType.O3)\n    ..m<$core.int, $core.int>(\n        22,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuAiRecommendedLevelV2Map',\n        entryClassName:\n            'DanmuPlayerConfig.PlayerDanmakuAiRecommendedLevelV2MapEntry',\n        keyFieldType: $pb.PbFieldType.O3,\n        valueFieldType: $pb.PbFieldType.O3,\n        packageName: const $pb.PackageName('bilibili.community.service.dm.v1'))\n    ..hasRequiredFields = false;\n\n  DanmuPlayerConfig._() : super();\n  factory DanmuPlayerConfig({\n    $core.bool? playerDanmakuSwitch,\n    $core.bool? playerDanmakuSwitchSave,\n    $core.bool? playerDanmakuUseDefaultConfig,\n    $core.bool? playerDanmakuAiRecommendedSwitch,\n    $core.int? playerDanmakuAiRecommendedLevel,\n    $core.bool? playerDanmakuBlocktop,\n    $core.bool? playerDanmakuBlockscroll,\n    $core.bool? playerDanmakuBlockbottom,\n    $core.bool? playerDanmakuBlockcolorful,\n    $core.bool? playerDanmakuBlockrepeat,\n    $core.bool? playerDanmakuBlockspecial,\n    $core.double? playerDanmakuOpacity,\n    $core.double? playerDanmakuScalingfactor,\n    $core.double? playerDanmakuDomain,\n    $core.int? playerDanmakuSpeed,\n    $core.bool? playerDanmakuEnableblocklist,\n    $core.bool? inlinePlayerDanmakuSwitch,\n    $core.int? inlinePlayerDanmakuConfig,\n    $core.int? playerDanmakuIosSwitchSave,\n    $core.int? playerDanmakuSeniorModeSwitch,\n    $core.int? playerDanmakuAiRecommendedLevelV2,\n    $core.Map<$core.int, $core.int>? playerDanmakuAiRecommendedLevelV2Map,\n  }) {\n    final _result = create();\n    if (playerDanmakuSwitch != null) {\n      _result.playerDanmakuSwitch = playerDanmakuSwitch;\n    }\n    if (playerDanmakuSwitchSave != null) {\n      _result.playerDanmakuSwitchSave = playerDanmakuSwitchSave;\n    }\n    if (playerDanmakuUseDefaultConfig != null) {\n      _result.playerDanmakuUseDefaultConfig = playerDanmakuUseDefaultConfig;\n    }\n    if (playerDanmakuAiRecommendedSwitch != null) {\n      _result.playerDanmakuAiRecommendedSwitch =\n          playerDanmakuAiRecommendedSwitch;\n    }\n    if (playerDanmakuAiRecommendedLevel != null) {\n      _result.playerDanmakuAiRecommendedLevel = playerDanmakuAiRecommendedLevel;\n    }\n    if (playerDanmakuBlocktop != null) {\n      _result.playerDanmakuBlocktop = playerDanmakuBlocktop;\n    }\n    if (playerDanmakuBlockscroll != null) {\n      _result.playerDanmakuBlockscroll = playerDanmakuBlockscroll;\n    }\n    if (playerDanmakuBlockbottom != null) {\n      _result.playerDanmakuBlockbottom = playerDanmakuBlockbottom;\n    }\n    if (playerDanmakuBlockcolorful != null) {\n      _result.playerDanmakuBlockcolorful = playerDanmakuBlockcolorful;\n    }\n    if (playerDanmakuBlockrepeat != null) {\n      _result.playerDanmakuBlockrepeat = playerDanmakuBlockrepeat;\n    }\n    if (playerDanmakuBlockspecial != null) {\n      _result.playerDanmakuBlockspecial = playerDanmakuBlockspecial;\n    }\n    if (playerDanmakuOpacity != null) {\n      _result.playerDanmakuOpacity = playerDanmakuOpacity;\n    }\n    if (playerDanmakuScalingfactor != null) {\n      _result.playerDanmakuScalingfactor = playerDanmakuScalingfactor;\n    }\n    if (playerDanmakuDomain != null) {\n      _result.playerDanmakuDomain = playerDanmakuDomain;\n    }\n    if (playerDanmakuSpeed != null) {\n      _result.playerDanmakuSpeed = playerDanmakuSpeed;\n    }\n    if (playerDanmakuEnableblocklist != null) {\n      _result.playerDanmakuEnableblocklist = playerDanmakuEnableblocklist;\n    }\n    if (inlinePlayerDanmakuSwitch != null) {\n      _result.inlinePlayerDanmakuSwitch = inlinePlayerDanmakuSwitch;\n    }\n    if (inlinePlayerDanmakuConfig != null) {\n      _result.inlinePlayerDanmakuConfig = inlinePlayerDanmakuConfig;\n    }\n    if (playerDanmakuIosSwitchSave != null) {\n      _result.playerDanmakuIosSwitchSave = playerDanmakuIosSwitchSave;\n    }\n    if (playerDanmakuSeniorModeSwitch != null) {\n      _result.playerDanmakuSeniorModeSwitch = playerDanmakuSeniorModeSwitch;\n    }\n    if (playerDanmakuAiRecommendedLevelV2 != null) {\n      _result.playerDanmakuAiRecommendedLevelV2 =\n          playerDanmakuAiRecommendedLevelV2;\n    }\n    if (playerDanmakuAiRecommendedLevelV2Map != null) {\n      _result.playerDanmakuAiRecommendedLevelV2Map\n          .addAll(playerDanmakuAiRecommendedLevelV2Map);\n    }\n    return _result;\n  }\n  factory DanmuPlayerConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmuPlayerConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmuPlayerConfig clone() => DanmuPlayerConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmuPlayerConfig copyWith(void Function(DanmuPlayerConfig) updates) =>\n      super.copyWith((message) => updates(message as DanmuPlayerConfig))\n          as DanmuPlayerConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmuPlayerConfig create() => DanmuPlayerConfig._();\n  DanmuPlayerConfig createEmptyInstance() => create();\n  static $pb.PbList<DanmuPlayerConfig> createRepeated() =>\n      $pb.PbList<DanmuPlayerConfig>();\n  @$core.pragma('dart2js:noInline')\n  static DanmuPlayerConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmuPlayerConfig>(create);\n  static DanmuPlayerConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get playerDanmakuSwitch => $_getBF(0);\n  @$pb.TagNumber(1)\n  set playerDanmakuSwitch($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasPlayerDanmakuSwitch() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearPlayerDanmakuSwitch() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.bool get playerDanmakuSwitchSave => $_getBF(1);\n  @$pb.TagNumber(2)\n  set playerDanmakuSwitchSave($core.bool v) {\n    $_setBool(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasPlayerDanmakuSwitchSave() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearPlayerDanmakuSwitchSave() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.bool get playerDanmakuUseDefaultConfig => $_getBF(2);\n  @$pb.TagNumber(3)\n  set playerDanmakuUseDefaultConfig($core.bool v) {\n    $_setBool(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasPlayerDanmakuUseDefaultConfig() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearPlayerDanmakuUseDefaultConfig() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.bool get playerDanmakuAiRecommendedSwitch => $_getBF(3);\n  @$pb.TagNumber(4)\n  set playerDanmakuAiRecommendedSwitch($core.bool v) {\n    $_setBool(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasPlayerDanmakuAiRecommendedSwitch() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearPlayerDanmakuAiRecommendedSwitch() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.int get playerDanmakuAiRecommendedLevel => $_getIZ(4);\n  @$pb.TagNumber(5)\n  set playerDanmakuAiRecommendedLevel($core.int v) {\n    $_setSignedInt32(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasPlayerDanmakuAiRecommendedLevel() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearPlayerDanmakuAiRecommendedLevel() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.bool get playerDanmakuBlocktop => $_getBF(5);\n  @$pb.TagNumber(6)\n  set playerDanmakuBlocktop($core.bool v) {\n    $_setBool(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasPlayerDanmakuBlocktop() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearPlayerDanmakuBlocktop() => clearField(6);\n\n  @$pb.TagNumber(7)\n  $core.bool get playerDanmakuBlockscroll => $_getBF(6);\n  @$pb.TagNumber(7)\n  set playerDanmakuBlockscroll($core.bool v) {\n    $_setBool(6, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasPlayerDanmakuBlockscroll() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearPlayerDanmakuBlockscroll() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $core.bool get playerDanmakuBlockbottom => $_getBF(7);\n  @$pb.TagNumber(8)\n  set playerDanmakuBlockbottom($core.bool v) {\n    $_setBool(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasPlayerDanmakuBlockbottom() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearPlayerDanmakuBlockbottom() => clearField(8);\n\n  @$pb.TagNumber(9)\n  $core.bool get playerDanmakuBlockcolorful => $_getBF(8);\n  @$pb.TagNumber(9)\n  set playerDanmakuBlockcolorful($core.bool v) {\n    $_setBool(8, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasPlayerDanmakuBlockcolorful() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearPlayerDanmakuBlockcolorful() => clearField(9);\n\n  @$pb.TagNumber(10)\n  $core.bool get playerDanmakuBlockrepeat => $_getBF(9);\n  @$pb.TagNumber(10)\n  set playerDanmakuBlockrepeat($core.bool v) {\n    $_setBool(9, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasPlayerDanmakuBlockrepeat() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearPlayerDanmakuBlockrepeat() => clearField(10);\n\n  @$pb.TagNumber(11)\n  $core.bool get playerDanmakuBlockspecial => $_getBF(10);\n  @$pb.TagNumber(11)\n  set playerDanmakuBlockspecial($core.bool v) {\n    $_setBool(10, v);\n  }\n\n  @$pb.TagNumber(11)\n  $core.bool hasPlayerDanmakuBlockspecial() => $_has(10);\n  @$pb.TagNumber(11)\n  void clearPlayerDanmakuBlockspecial() => clearField(11);\n\n  @$pb.TagNumber(12)\n  $core.double get playerDanmakuOpacity => $_getN(11);\n  @$pb.TagNumber(12)\n  set playerDanmakuOpacity($core.double v) {\n    $_setFloat(11, v);\n  }\n\n  @$pb.TagNumber(12)\n  $core.bool hasPlayerDanmakuOpacity() => $_has(11);\n  @$pb.TagNumber(12)\n  void clearPlayerDanmakuOpacity() => clearField(12);\n\n  @$pb.TagNumber(13)\n  $core.double get playerDanmakuScalingfactor => $_getN(12);\n  @$pb.TagNumber(13)\n  set playerDanmakuScalingfactor($core.double v) {\n    $_setFloat(12, v);\n  }\n\n  @$pb.TagNumber(13)\n  $core.bool hasPlayerDanmakuScalingfactor() => $_has(12);\n  @$pb.TagNumber(13)\n  void clearPlayerDanmakuScalingfactor() => clearField(13);\n\n  @$pb.TagNumber(14)\n  $core.double get playerDanmakuDomain => $_getN(13);\n  @$pb.TagNumber(14)\n  set playerDanmakuDomain($core.double v) {\n    $_setFloat(13, v);\n  }\n\n  @$pb.TagNumber(14)\n  $core.bool hasPlayerDanmakuDomain() => $_has(13);\n  @$pb.TagNumber(14)\n  void clearPlayerDanmakuDomain() => clearField(14);\n\n  @$pb.TagNumber(15)\n  $core.int get playerDanmakuSpeed => $_getIZ(14);\n  @$pb.TagNumber(15)\n  set playerDanmakuSpeed($core.int v) {\n    $_setSignedInt32(14, v);\n  }\n\n  @$pb.TagNumber(15)\n  $core.bool hasPlayerDanmakuSpeed() => $_has(14);\n  @$pb.TagNumber(15)\n  void clearPlayerDanmakuSpeed() => clearField(15);\n\n  @$pb.TagNumber(16)\n  $core.bool get playerDanmakuEnableblocklist => $_getBF(15);\n  @$pb.TagNumber(16)\n  set playerDanmakuEnableblocklist($core.bool v) {\n    $_setBool(15, v);\n  }\n\n  @$pb.TagNumber(16)\n  $core.bool hasPlayerDanmakuEnableblocklist() => $_has(15);\n  @$pb.TagNumber(16)\n  void clearPlayerDanmakuEnableblocklist() => clearField(16);\n\n  @$pb.TagNumber(17)\n  $core.bool get inlinePlayerDanmakuSwitch => $_getBF(16);\n  @$pb.TagNumber(17)\n  set inlinePlayerDanmakuSwitch($core.bool v) {\n    $_setBool(16, v);\n  }\n\n  @$pb.TagNumber(17)\n  $core.bool hasInlinePlayerDanmakuSwitch() => $_has(16);\n  @$pb.TagNumber(17)\n  void clearInlinePlayerDanmakuSwitch() => clearField(17);\n\n  @$pb.TagNumber(18)\n  $core.int get inlinePlayerDanmakuConfig => $_getIZ(17);\n  @$pb.TagNumber(18)\n  set inlinePlayerDanmakuConfig($core.int v) {\n    $_setSignedInt32(17, v);\n  }\n\n  @$pb.TagNumber(18)\n  $core.bool hasInlinePlayerDanmakuConfig() => $_has(17);\n  @$pb.TagNumber(18)\n  void clearInlinePlayerDanmakuConfig() => clearField(18);\n\n  @$pb.TagNumber(19)\n  $core.int get playerDanmakuIosSwitchSave => $_getIZ(18);\n  @$pb.TagNumber(19)\n  set playerDanmakuIosSwitchSave($core.int v) {\n    $_setSignedInt32(18, v);\n  }\n\n  @$pb.TagNumber(19)\n  $core.bool hasPlayerDanmakuIosSwitchSave() => $_has(18);\n  @$pb.TagNumber(19)\n  void clearPlayerDanmakuIosSwitchSave() => clearField(19);\n\n  @$pb.TagNumber(20)\n  $core.int get playerDanmakuSeniorModeSwitch => $_getIZ(19);\n  @$pb.TagNumber(20)\n  set playerDanmakuSeniorModeSwitch($core.int v) {\n    $_setSignedInt32(19, v);\n  }\n\n  @$pb.TagNumber(20)\n  $core.bool hasPlayerDanmakuSeniorModeSwitch() => $_has(19);\n  @$pb.TagNumber(20)\n  void clearPlayerDanmakuSeniorModeSwitch() => clearField(20);\n\n  @$pb.TagNumber(21)\n  $core.int get playerDanmakuAiRecommendedLevelV2 => $_getIZ(20);\n  @$pb.TagNumber(21)\n  set playerDanmakuAiRecommendedLevelV2($core.int v) {\n    $_setSignedInt32(20, v);\n  }\n\n  @$pb.TagNumber(21)\n  $core.bool hasPlayerDanmakuAiRecommendedLevelV2() => $_has(20);\n  @$pb.TagNumber(21)\n  void clearPlayerDanmakuAiRecommendedLevelV2() => clearField(21);\n\n  @$pb.TagNumber(22)\n  $core.Map<$core.int, $core.int> get playerDanmakuAiRecommendedLevelV2Map =>\n      $_getMap(21);\n}\n\nclass DanmuPlayerConfigPanel extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmuPlayerConfigPanel',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'selectionText')\n    ..hasRequiredFields = false;\n\n  DanmuPlayerConfigPanel._() : super();\n  factory DanmuPlayerConfigPanel({\n    $core.String? selectionText,\n  }) {\n    final _result = create();\n    if (selectionText != null) {\n      _result.selectionText = selectionText;\n    }\n    return _result;\n  }\n  factory DanmuPlayerConfigPanel.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmuPlayerConfigPanel.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmuPlayerConfigPanel clone() =>\n      DanmuPlayerConfigPanel()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmuPlayerConfigPanel copyWith(\n          void Function(DanmuPlayerConfigPanel) updates) =>\n      super.copyWith((message) => updates(message as DanmuPlayerConfigPanel))\n          as DanmuPlayerConfigPanel; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmuPlayerConfigPanel create() => DanmuPlayerConfigPanel._();\n  DanmuPlayerConfigPanel createEmptyInstance() => create();\n  static $pb.PbList<DanmuPlayerConfigPanel> createRepeated() =>\n      $pb.PbList<DanmuPlayerConfigPanel>();\n  @$core.pragma('dart2js:noInline')\n  static DanmuPlayerConfigPanel getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmuPlayerConfigPanel>(create);\n  static DanmuPlayerConfigPanel? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get selectionText => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set selectionText($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasSelectionText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearSelectionText() => clearField(1);\n}\n\nclass DanmuPlayerDynamicConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmuPlayerDynamicConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.int>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'progress',\n        $pb.PbFieldType.O3)\n    ..a<$core.double>(\n        14,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerDanmakuDomain',\n        $pb.PbFieldType.OF)\n    ..hasRequiredFields = false;\n\n  DanmuPlayerDynamicConfig._() : super();\n  factory DanmuPlayerDynamicConfig({\n    $core.int? progress,\n    $core.double? playerDanmakuDomain,\n  }) {\n    final _result = create();\n    if (progress != null) {\n      _result.progress = progress;\n    }\n    if (playerDanmakuDomain != null) {\n      _result.playerDanmakuDomain = playerDanmakuDomain;\n    }\n    return _result;\n  }\n  factory DanmuPlayerDynamicConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmuPlayerDynamicConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmuPlayerDynamicConfig clone() =>\n      DanmuPlayerDynamicConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmuPlayerDynamicConfig copyWith(\n          void Function(DanmuPlayerDynamicConfig) updates) =>\n      super.copyWith((message) => updates(message as DanmuPlayerDynamicConfig))\n          as DanmuPlayerDynamicConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmuPlayerDynamicConfig create() => DanmuPlayerDynamicConfig._();\n  DanmuPlayerDynamicConfig createEmptyInstance() => create();\n  static $pb.PbList<DanmuPlayerDynamicConfig> createRepeated() =>\n      $pb.PbList<DanmuPlayerDynamicConfig>();\n  @$core.pragma('dart2js:noInline')\n  static DanmuPlayerDynamicConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmuPlayerDynamicConfig>(create);\n  static DanmuPlayerDynamicConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.int get progress => $_getIZ(0);\n  @$pb.TagNumber(1)\n  set progress($core.int v) {\n    $_setSignedInt32(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasProgress() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearProgress() => clearField(1);\n\n  @$pb.TagNumber(14)\n  $core.double get playerDanmakuDomain => $_getN(1);\n  @$pb.TagNumber(14)\n  set playerDanmakuDomain($core.double v) {\n    $_setFloat(1, v);\n  }\n\n  @$pb.TagNumber(14)\n  $core.bool hasPlayerDanmakuDomain() => $_has(1);\n  @$pb.TagNumber(14)\n  void clearPlayerDanmakuDomain() => clearField(14);\n}\n\nclass DanmuPlayerViewConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmuPlayerViewConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOM<DanmuDefaultPlayerConfig>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'danmukuDefaultPlayerConfig',\n        subBuilder: DanmuDefaultPlayerConfig.create)\n    ..aOM<DanmuPlayerConfig>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'danmukuPlayerConfig',\n        subBuilder: DanmuPlayerConfig.create)\n    ..pc<DanmuPlayerDynamicConfig>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'danmukuPlayerDynamicConfig',\n        $pb.PbFieldType.PM,\n        subBuilder: DanmuPlayerDynamicConfig.create)\n    ..aOM<DanmuPlayerConfigPanel>(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'danmukuPlayerConfigPanel',\n        subBuilder: DanmuPlayerConfigPanel.create)\n    ..hasRequiredFields = false;\n\n  DanmuPlayerViewConfig._() : super();\n  factory DanmuPlayerViewConfig({\n    DanmuDefaultPlayerConfig? danmukuDefaultPlayerConfig,\n    DanmuPlayerConfig? danmukuPlayerConfig,\n    $core.Iterable<DanmuPlayerDynamicConfig>? danmukuPlayerDynamicConfig,\n    DanmuPlayerConfigPanel? danmukuPlayerConfigPanel,\n  }) {\n    final _result = create();\n    if (danmukuDefaultPlayerConfig != null) {\n      _result.danmukuDefaultPlayerConfig = danmukuDefaultPlayerConfig;\n    }\n    if (danmukuPlayerConfig != null) {\n      _result.danmukuPlayerConfig = danmukuPlayerConfig;\n    }\n    if (danmukuPlayerDynamicConfig != null) {\n      _result.danmukuPlayerDynamicConfig.addAll(danmukuPlayerDynamicConfig);\n    }\n    if (danmukuPlayerConfigPanel != null) {\n      _result.danmukuPlayerConfigPanel = danmukuPlayerConfigPanel;\n    }\n    return _result;\n  }\n  factory DanmuPlayerViewConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmuPlayerViewConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmuPlayerViewConfig clone() =>\n      DanmuPlayerViewConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmuPlayerViewConfig copyWith(\n          void Function(DanmuPlayerViewConfig) updates) =>\n      super.copyWith((message) => updates(message as DanmuPlayerViewConfig))\n          as DanmuPlayerViewConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmuPlayerViewConfig create() => DanmuPlayerViewConfig._();\n  DanmuPlayerViewConfig createEmptyInstance() => create();\n  static $pb.PbList<DanmuPlayerViewConfig> createRepeated() =>\n      $pb.PbList<DanmuPlayerViewConfig>();\n  @$core.pragma('dart2js:noInline')\n  static DanmuPlayerViewConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmuPlayerViewConfig>(create);\n  static DanmuPlayerViewConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  DanmuDefaultPlayerConfig get danmukuDefaultPlayerConfig => $_getN(0);\n  @$pb.TagNumber(1)\n  set danmukuDefaultPlayerConfig(DanmuDefaultPlayerConfig v) {\n    setField(1, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasDanmukuDefaultPlayerConfig() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearDanmukuDefaultPlayerConfig() => clearField(1);\n  @$pb.TagNumber(1)\n  DanmuDefaultPlayerConfig ensureDanmukuDefaultPlayerConfig() => $_ensure(0);\n\n  @$pb.TagNumber(2)\n  DanmuPlayerConfig get danmukuPlayerConfig => $_getN(1);\n  @$pb.TagNumber(2)\n  set danmukuPlayerConfig(DanmuPlayerConfig v) {\n    setField(2, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasDanmukuPlayerConfig() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearDanmukuPlayerConfig() => clearField(2);\n  @$pb.TagNumber(2)\n  DanmuPlayerConfig ensureDanmukuPlayerConfig() => $_ensure(1);\n\n  @$pb.TagNumber(3)\n  $core.List<DanmuPlayerDynamicConfig> get danmukuPlayerDynamicConfig =>\n      $_getList(2);\n\n  @$pb.TagNumber(4)\n  DanmuPlayerConfigPanel get danmukuPlayerConfigPanel => $_getN(3);\n  @$pb.TagNumber(4)\n  set danmukuPlayerConfigPanel(DanmuPlayerConfigPanel v) {\n    setField(4, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasDanmukuPlayerConfigPanel() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearDanmukuPlayerConfigPanel() => clearField(4);\n  @$pb.TagNumber(4)\n  DanmuPlayerConfigPanel ensureDanmukuPlayerConfigPanel() => $_ensure(3);\n}\n\nclass DanmuWebPlayerConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DanmuWebPlayerConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'dmSwitch')\n    ..aOB(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiSwitch')\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiLevel',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blocktop')\n    ..aOB(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockscroll')\n    ..aOB(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockbottom')\n    ..aOB(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockcolor')\n    ..aOB(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockspecial')\n    ..aOB(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'preventshade')\n    ..aOB(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'dmask')\n    ..a<$core.double>(\n        11,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'opacity',\n        $pb.PbFieldType.OF)\n    ..a<$core.int>(\n        12,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'dmarea',\n        $pb.PbFieldType.O3)\n    ..a<$core.double>(\n        13,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'speedplus',\n        $pb.PbFieldType.OF)\n    ..a<$core.double>(\n        14,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'fontsize',\n        $pb.PbFieldType.OF)\n    ..aOB(\n        15,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'screensync')\n    ..aOB(\n        16,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'speedsync')\n    ..aOS(\n        17,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'fontfamily')\n    ..aOB(\n        18,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'bold')\n    ..a<$core.int>(\n        19,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'fontborder',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        20,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'drawType')\n    ..a<$core.int>(\n        21,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'seniorModeSwitch',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        22,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiLevelV2',\n        $pb.PbFieldType.O3)\n    ..m<$core.int, $core.int>(\n        23,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiLevelV2Map',\n        entryClassName: 'DanmuWebPlayerConfig.AiLevelV2MapEntry',\n        keyFieldType: $pb.PbFieldType.O3,\n        valueFieldType: $pb.PbFieldType.O3,\n        packageName: const $pb.PackageName('bilibili.community.service.dm.v1'))\n    ..hasRequiredFields = false;\n\n  DanmuWebPlayerConfig._() : super();\n  factory DanmuWebPlayerConfig({\n    $core.bool? dmSwitch,\n    $core.bool? aiSwitch,\n    $core.int? aiLevel,\n    $core.bool? blocktop,\n    $core.bool? blockscroll,\n    $core.bool? blockbottom,\n    $core.bool? blockcolor,\n    $core.bool? blockspecial,\n    $core.bool? preventshade,\n    $core.bool? dmask,\n    $core.double? opacity,\n    $core.int? dmarea,\n    $core.double? speedplus,\n    $core.double? fontsize,\n    $core.bool? screensync,\n    $core.bool? speedsync,\n    $core.String? fontfamily,\n    $core.bool? bold,\n    $core.int? fontborder,\n    $core.String? drawType,\n    $core.int? seniorModeSwitch,\n    $core.int? aiLevelV2,\n    $core.Map<$core.int, $core.int>? aiLevelV2Map,\n  }) {\n    final _result = create();\n    if (dmSwitch != null) {\n      _result.dmSwitch = dmSwitch;\n    }\n    if (aiSwitch != null) {\n      _result.aiSwitch = aiSwitch;\n    }\n    if (aiLevel != null) {\n      _result.aiLevel = aiLevel;\n    }\n    if (blocktop != null) {\n      _result.blocktop = blocktop;\n    }\n    if (blockscroll != null) {\n      _result.blockscroll = blockscroll;\n    }\n    if (blockbottom != null) {\n      _result.blockbottom = blockbottom;\n    }\n    if (blockcolor != null) {\n      _result.blockcolor = blockcolor;\n    }\n    if (blockspecial != null) {\n      _result.blockspecial = blockspecial;\n    }\n    if (preventshade != null) {\n      _result.preventshade = preventshade;\n    }\n    if (dmask != null) {\n      _result.dmask = dmask;\n    }\n    if (opacity != null) {\n      _result.opacity = opacity;\n    }\n    if (dmarea != null) {\n      _result.dmarea = dmarea;\n    }\n    if (speedplus != null) {\n      _result.speedplus = speedplus;\n    }\n    if (fontsize != null) {\n      _result.fontsize = fontsize;\n    }\n    if (screensync != null) {\n      _result.screensync = screensync;\n    }\n    if (speedsync != null) {\n      _result.speedsync = speedsync;\n    }\n    if (fontfamily != null) {\n      _result.fontfamily = fontfamily;\n    }\n    if (bold != null) {\n      _result.bold = bold;\n    }\n    if (fontborder != null) {\n      _result.fontborder = fontborder;\n    }\n    if (drawType != null) {\n      _result.drawType = drawType;\n    }\n    if (seniorModeSwitch != null) {\n      _result.seniorModeSwitch = seniorModeSwitch;\n    }\n    if (aiLevelV2 != null) {\n      _result.aiLevelV2 = aiLevelV2;\n    }\n    if (aiLevelV2Map != null) {\n      _result.aiLevelV2Map.addAll(aiLevelV2Map);\n    }\n    return _result;\n  }\n  factory DanmuWebPlayerConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DanmuWebPlayerConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DanmuWebPlayerConfig clone() =>\n      DanmuWebPlayerConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DanmuWebPlayerConfig copyWith(void Function(DanmuWebPlayerConfig) updates) =>\n      super.copyWith((message) => updates(message as DanmuWebPlayerConfig))\n          as DanmuWebPlayerConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DanmuWebPlayerConfig create() => DanmuWebPlayerConfig._();\n  DanmuWebPlayerConfig createEmptyInstance() => create();\n  static $pb.PbList<DanmuWebPlayerConfig> createRepeated() =>\n      $pb.PbList<DanmuWebPlayerConfig>();\n  @$core.pragma('dart2js:noInline')\n  static DanmuWebPlayerConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DanmuWebPlayerConfig>(create);\n  static DanmuWebPlayerConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get dmSwitch => $_getBF(0);\n  @$pb.TagNumber(1)\n  set dmSwitch($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasDmSwitch() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearDmSwitch() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.bool get aiSwitch => $_getBF(1);\n  @$pb.TagNumber(2)\n  set aiSwitch($core.bool v) {\n    $_setBool(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasAiSwitch() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearAiSwitch() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get aiLevel => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set aiLevel($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasAiLevel() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearAiLevel() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.bool get blocktop => $_getBF(3);\n  @$pb.TagNumber(4)\n  set blocktop($core.bool v) {\n    $_setBool(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasBlocktop() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearBlocktop() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.bool get blockscroll => $_getBF(4);\n  @$pb.TagNumber(5)\n  set blockscroll($core.bool v) {\n    $_setBool(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasBlockscroll() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearBlockscroll() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.bool get blockbottom => $_getBF(5);\n  @$pb.TagNumber(6)\n  set blockbottom($core.bool v) {\n    $_setBool(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasBlockbottom() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearBlockbottom() => clearField(6);\n\n  @$pb.TagNumber(7)\n  $core.bool get blockcolor => $_getBF(6);\n  @$pb.TagNumber(7)\n  set blockcolor($core.bool v) {\n    $_setBool(6, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasBlockcolor() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearBlockcolor() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $core.bool get blockspecial => $_getBF(7);\n  @$pb.TagNumber(8)\n  set blockspecial($core.bool v) {\n    $_setBool(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasBlockspecial() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearBlockspecial() => clearField(8);\n\n  @$pb.TagNumber(9)\n  $core.bool get preventshade => $_getBF(8);\n  @$pb.TagNumber(9)\n  set preventshade($core.bool v) {\n    $_setBool(8, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasPreventshade() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearPreventshade() => clearField(9);\n\n  @$pb.TagNumber(10)\n  $core.bool get dmask => $_getBF(9);\n  @$pb.TagNumber(10)\n  set dmask($core.bool v) {\n    $_setBool(9, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasDmask() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearDmask() => clearField(10);\n\n  @$pb.TagNumber(11)\n  $core.double get opacity => $_getN(10);\n  @$pb.TagNumber(11)\n  set opacity($core.double v) {\n    $_setFloat(10, v);\n  }\n\n  @$pb.TagNumber(11)\n  $core.bool hasOpacity() => $_has(10);\n  @$pb.TagNumber(11)\n  void clearOpacity() => clearField(11);\n\n  @$pb.TagNumber(12)\n  $core.int get dmarea => $_getIZ(11);\n  @$pb.TagNumber(12)\n  set dmarea($core.int v) {\n    $_setSignedInt32(11, v);\n  }\n\n  @$pb.TagNumber(12)\n  $core.bool hasDmarea() => $_has(11);\n  @$pb.TagNumber(12)\n  void clearDmarea() => clearField(12);\n\n  @$pb.TagNumber(13)\n  $core.double get speedplus => $_getN(12);\n  @$pb.TagNumber(13)\n  set speedplus($core.double v) {\n    $_setFloat(12, v);\n  }\n\n  @$pb.TagNumber(13)\n  $core.bool hasSpeedplus() => $_has(12);\n  @$pb.TagNumber(13)\n  void clearSpeedplus() => clearField(13);\n\n  @$pb.TagNumber(14)\n  $core.double get fontsize => $_getN(13);\n  @$pb.TagNumber(14)\n  set fontsize($core.double v) {\n    $_setFloat(13, v);\n  }\n\n  @$pb.TagNumber(14)\n  $core.bool hasFontsize() => $_has(13);\n  @$pb.TagNumber(14)\n  void clearFontsize() => clearField(14);\n\n  @$pb.TagNumber(15)\n  $core.bool get screensync => $_getBF(14);\n  @$pb.TagNumber(15)\n  set screensync($core.bool v) {\n    $_setBool(14, v);\n  }\n\n  @$pb.TagNumber(15)\n  $core.bool hasScreensync() => $_has(14);\n  @$pb.TagNumber(15)\n  void clearScreensync() => clearField(15);\n\n  @$pb.TagNumber(16)\n  $core.bool get speedsync => $_getBF(15);\n  @$pb.TagNumber(16)\n  set speedsync($core.bool v) {\n    $_setBool(15, v);\n  }\n\n  @$pb.TagNumber(16)\n  $core.bool hasSpeedsync() => $_has(15);\n  @$pb.TagNumber(16)\n  void clearSpeedsync() => clearField(16);\n\n  @$pb.TagNumber(17)\n  $core.String get fontfamily => $_getSZ(16);\n  @$pb.TagNumber(17)\n  set fontfamily($core.String v) {\n    $_setString(16, v);\n  }\n\n  @$pb.TagNumber(17)\n  $core.bool hasFontfamily() => $_has(16);\n  @$pb.TagNumber(17)\n  void clearFontfamily() => clearField(17);\n\n  @$pb.TagNumber(18)\n  $core.bool get bold => $_getBF(17);\n  @$pb.TagNumber(18)\n  set bold($core.bool v) {\n    $_setBool(17, v);\n  }\n\n  @$pb.TagNumber(18)\n  $core.bool hasBold() => $_has(17);\n  @$pb.TagNumber(18)\n  void clearBold() => clearField(18);\n\n  @$pb.TagNumber(19)\n  $core.int get fontborder => $_getIZ(18);\n  @$pb.TagNumber(19)\n  set fontborder($core.int v) {\n    $_setSignedInt32(18, v);\n  }\n\n  @$pb.TagNumber(19)\n  $core.bool hasFontborder() => $_has(18);\n  @$pb.TagNumber(19)\n  void clearFontborder() => clearField(19);\n\n  @$pb.TagNumber(20)\n  $core.String get drawType => $_getSZ(19);\n  @$pb.TagNumber(20)\n  set drawType($core.String v) {\n    $_setString(19, v);\n  }\n\n  @$pb.TagNumber(20)\n  $core.bool hasDrawType() => $_has(19);\n  @$pb.TagNumber(20)\n  void clearDrawType() => clearField(20);\n\n  @$pb.TagNumber(21)\n  $core.int get seniorModeSwitch => $_getIZ(20);\n  @$pb.TagNumber(21)\n  set seniorModeSwitch($core.int v) {\n    $_setSignedInt32(20, v);\n  }\n\n  @$pb.TagNumber(21)\n  $core.bool hasSeniorModeSwitch() => $_has(20);\n  @$pb.TagNumber(21)\n  void clearSeniorModeSwitch() => clearField(21);\n\n  @$pb.TagNumber(22)\n  $core.int get aiLevelV2 => $_getIZ(21);\n  @$pb.TagNumber(22)\n  set aiLevelV2($core.int v) {\n    $_setSignedInt32(21, v);\n  }\n\n  @$pb.TagNumber(22)\n  $core.bool hasAiLevelV2() => $_has(21);\n  @$pb.TagNumber(22)\n  void clearAiLevelV2() => clearField(22);\n\n  @$pb.TagNumber(23)\n  $core.Map<$core.int, $core.int> get aiLevelV2Map => $_getMap(22);\n}\n\nclass DmExpoReportReq extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmExpoReportReq',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'sessionId')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'oid')\n    ..aOS(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'spmid')\n    ..hasRequiredFields = false;\n\n  DmExpoReportReq._() : super();\n  factory DmExpoReportReq({\n    $core.String? sessionId,\n    $fixnum.Int64? oid,\n    $core.String? spmid,\n  }) {\n    final _result = create();\n    if (sessionId != null) {\n      _result.sessionId = sessionId;\n    }\n    if (oid != null) {\n      _result.oid = oid;\n    }\n    if (spmid != null) {\n      _result.spmid = spmid;\n    }\n    return _result;\n  }\n  factory DmExpoReportReq.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmExpoReportReq.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmExpoReportReq clone() => DmExpoReportReq()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmExpoReportReq copyWith(void Function(DmExpoReportReq) updates) =>\n      super.copyWith((message) => updates(message as DmExpoReportReq))\n          as DmExpoReportReq; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmExpoReportReq create() => DmExpoReportReq._();\n  DmExpoReportReq createEmptyInstance() => create();\n  static $pb.PbList<DmExpoReportReq> createRepeated() =>\n      $pb.PbList<DmExpoReportReq>();\n  @$core.pragma('dart2js:noInline')\n  static DmExpoReportReq getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmExpoReportReq>(create);\n  static DmExpoReportReq? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get sessionId => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set sessionId($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasSessionId() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearSessionId() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get oid => $_getI64(1);\n  @$pb.TagNumber(2)\n  set oid($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasOid() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearOid() => clearField(2);\n\n  @$pb.TagNumber(4)\n  $core.String get spmid => $_getSZ(2);\n  @$pb.TagNumber(4)\n  set spmid($core.String v) {\n    $_setString(2, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasSpmid() => $_has(2);\n  @$pb.TagNumber(4)\n  void clearSpmid() => clearField(4);\n}\n\nclass DmExpoReportRes extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmExpoReportRes',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..hasRequiredFields = false;\n\n  DmExpoReportRes._() : super();\n  factory DmExpoReportRes() => create();\n  factory DmExpoReportRes.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmExpoReportRes.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmExpoReportRes clone() => DmExpoReportRes()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmExpoReportRes copyWith(void Function(DmExpoReportRes) updates) =>\n      super.copyWith((message) => updates(message as DmExpoReportRes))\n          as DmExpoReportRes; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmExpoReportRes create() => DmExpoReportRes._();\n  DmExpoReportRes createEmptyInstance() => create();\n  static $pb.PbList<DmExpoReportRes> createRepeated() =>\n      $pb.PbList<DmExpoReportRes>();\n  @$core.pragma('dart2js:noInline')\n  static DmExpoReportRes getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmExpoReportRes>(create);\n  static DmExpoReportRes? _defaultInstance;\n}\n\nclass DmPlayerConfigReq extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmPlayerConfigReq',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'ts')\n    ..aOM<PlayerDanmakuSwitch>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'switch',\n        subBuilder: PlayerDanmakuSwitch.create)\n    ..aOM<PlayerDanmakuSwitchSave>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'switchSave',\n        subBuilder: PlayerDanmakuSwitchSave.create)\n    ..aOM<PlayerDanmakuUseDefaultConfig>(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'useDefaultConfig',\n        subBuilder: PlayerDanmakuUseDefaultConfig.create)\n    ..aOM<PlayerDanmakuAiRecommendedSwitch>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiRecommendedSwitch',\n        subBuilder: PlayerDanmakuAiRecommendedSwitch.create)\n    ..aOM<PlayerDanmakuAiRecommendedLevel>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiRecommendedLevel',\n        subBuilder: PlayerDanmakuAiRecommendedLevel.create)\n    ..aOM<PlayerDanmakuBlocktop>(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blocktop',\n        subBuilder: PlayerDanmakuBlocktop.create)\n    ..aOM<PlayerDanmakuBlockscroll>(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockscroll',\n        subBuilder: PlayerDanmakuBlockscroll.create)\n    ..aOM<PlayerDanmakuBlockbottom>(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockbottom',\n        subBuilder: PlayerDanmakuBlockbottom.create)\n    ..aOM<PlayerDanmakuBlockcolorful>(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockcolorful',\n        subBuilder: PlayerDanmakuBlockcolorful.create)\n    ..aOM<PlayerDanmakuBlockrepeat>(\n        11,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockrepeat',\n        subBuilder: PlayerDanmakuBlockrepeat.create)\n    ..aOM<PlayerDanmakuBlockspecial>(\n        12,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'blockspecial',\n        subBuilder: PlayerDanmakuBlockspecial.create)\n    ..aOM<PlayerDanmakuOpacity>(\n        13,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'opacity',\n        subBuilder: PlayerDanmakuOpacity.create)\n    ..aOM<PlayerDanmakuScalingfactor>(\n        14,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'scalingfactor',\n        subBuilder: PlayerDanmakuScalingfactor.create)\n    ..aOM<PlayerDanmakuDomain>(\n        15,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'domain',\n        subBuilder: PlayerDanmakuDomain.create)\n    ..aOM<PlayerDanmakuSpeed>(\n        16,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'speed',\n        subBuilder: PlayerDanmakuSpeed.create)\n    ..aOM<PlayerDanmakuEnableblocklist>(\n        17,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'enableblocklist',\n        subBuilder: PlayerDanmakuEnableblocklist.create)\n    ..aOM<InlinePlayerDanmakuSwitch>(\n        18,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'inlinePlayerDanmakuSwitch',\n        protoName: 'inlinePlayerDanmakuSwitch',\n        subBuilder: InlinePlayerDanmakuSwitch.create)\n    ..aOM<PlayerDanmakuSeniorModeSwitch>(\n        19,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'seniorModeSwitch',\n        subBuilder: PlayerDanmakuSeniorModeSwitch.create)\n    ..aOM<PlayerDanmakuAiRecommendedLevelV2>(\n        20,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiRecommendedLevelV2',\n        subBuilder: PlayerDanmakuAiRecommendedLevelV2.create)\n    ..hasRequiredFields = false;\n\n  DmPlayerConfigReq._() : super();\n  factory DmPlayerConfigReq({\n    $fixnum.Int64? ts,\n    PlayerDanmakuSwitch? switch_2,\n    PlayerDanmakuSwitchSave? switchSave,\n    PlayerDanmakuUseDefaultConfig? useDefaultConfig,\n    PlayerDanmakuAiRecommendedSwitch? aiRecommendedSwitch,\n    PlayerDanmakuAiRecommendedLevel? aiRecommendedLevel,\n    PlayerDanmakuBlocktop? blocktop,\n    PlayerDanmakuBlockscroll? blockscroll,\n    PlayerDanmakuBlockbottom? blockbottom,\n    PlayerDanmakuBlockcolorful? blockcolorful,\n    PlayerDanmakuBlockrepeat? blockrepeat,\n    PlayerDanmakuBlockspecial? blockspecial,\n    PlayerDanmakuOpacity? opacity,\n    PlayerDanmakuScalingfactor? scalingfactor,\n    PlayerDanmakuDomain? domain,\n    PlayerDanmakuSpeed? speed,\n    PlayerDanmakuEnableblocklist? enableblocklist,\n    InlinePlayerDanmakuSwitch? inlinePlayerDanmakuSwitch,\n    PlayerDanmakuSeniorModeSwitch? seniorModeSwitch,\n    PlayerDanmakuAiRecommendedLevelV2? aiRecommendedLevelV2,\n  }) {\n    final _result = create();\n    if (ts != null) {\n      _result.ts = ts;\n    }\n    if (switch_2 != null) {\n      _result.switch_2 = switch_2;\n    }\n    if (switchSave != null) {\n      _result.switchSave = switchSave;\n    }\n    if (useDefaultConfig != null) {\n      _result.useDefaultConfig = useDefaultConfig;\n    }\n    if (aiRecommendedSwitch != null) {\n      _result.aiRecommendedSwitch = aiRecommendedSwitch;\n    }\n    if (aiRecommendedLevel != null) {\n      _result.aiRecommendedLevel = aiRecommendedLevel;\n    }\n    if (blocktop != null) {\n      _result.blocktop = blocktop;\n    }\n    if (blockscroll != null) {\n      _result.blockscroll = blockscroll;\n    }\n    if (blockbottom != null) {\n      _result.blockbottom = blockbottom;\n    }\n    if (blockcolorful != null) {\n      _result.blockcolorful = blockcolorful;\n    }\n    if (blockrepeat != null) {\n      _result.blockrepeat = blockrepeat;\n    }\n    if (blockspecial != null) {\n      _result.blockspecial = blockspecial;\n    }\n    if (opacity != null) {\n      _result.opacity = opacity;\n    }\n    if (scalingfactor != null) {\n      _result.scalingfactor = scalingfactor;\n    }\n    if (domain != null) {\n      _result.domain = domain;\n    }\n    if (speed != null) {\n      _result.speed = speed;\n    }\n    if (enableblocklist != null) {\n      _result.enableblocklist = enableblocklist;\n    }\n    if (inlinePlayerDanmakuSwitch != null) {\n      _result.inlinePlayerDanmakuSwitch = inlinePlayerDanmakuSwitch;\n    }\n    if (seniorModeSwitch != null) {\n      _result.seniorModeSwitch = seniorModeSwitch;\n    }\n    if (aiRecommendedLevelV2 != null) {\n      _result.aiRecommendedLevelV2 = aiRecommendedLevelV2;\n    }\n    return _result;\n  }\n  factory DmPlayerConfigReq.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmPlayerConfigReq.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmPlayerConfigReq clone() => DmPlayerConfigReq()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmPlayerConfigReq copyWith(void Function(DmPlayerConfigReq) updates) =>\n      super.copyWith((message) => updates(message as DmPlayerConfigReq))\n          as DmPlayerConfigReq; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmPlayerConfigReq create() => DmPlayerConfigReq._();\n  DmPlayerConfigReq createEmptyInstance() => create();\n  static $pb.PbList<DmPlayerConfigReq> createRepeated() =>\n      $pb.PbList<DmPlayerConfigReq>();\n  @$core.pragma('dart2js:noInline')\n  static DmPlayerConfigReq getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmPlayerConfigReq>(create);\n  static DmPlayerConfigReq? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get ts => $_getI64(0);\n  @$pb.TagNumber(1)\n  set ts($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasTs() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearTs() => clearField(1);\n\n  @$pb.TagNumber(2)\n  PlayerDanmakuSwitch get switch_2 => $_getN(1);\n  @$pb.TagNumber(2)\n  set switch_2(PlayerDanmakuSwitch v) {\n    setField(2, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasSwitch_2() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearSwitch_2() => clearField(2);\n  @$pb.TagNumber(2)\n  PlayerDanmakuSwitch ensureSwitch_2() => $_ensure(1);\n\n  @$pb.TagNumber(3)\n  PlayerDanmakuSwitchSave get switchSave => $_getN(2);\n  @$pb.TagNumber(3)\n  set switchSave(PlayerDanmakuSwitchSave v) {\n    setField(3, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasSwitchSave() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearSwitchSave() => clearField(3);\n  @$pb.TagNumber(3)\n  PlayerDanmakuSwitchSave ensureSwitchSave() => $_ensure(2);\n\n  @$pb.TagNumber(4)\n  PlayerDanmakuUseDefaultConfig get useDefaultConfig => $_getN(3);\n  @$pb.TagNumber(4)\n  set useDefaultConfig(PlayerDanmakuUseDefaultConfig v) {\n    setField(4, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasUseDefaultConfig() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearUseDefaultConfig() => clearField(4);\n  @$pb.TagNumber(4)\n  PlayerDanmakuUseDefaultConfig ensureUseDefaultConfig() => $_ensure(3);\n\n  @$pb.TagNumber(5)\n  PlayerDanmakuAiRecommendedSwitch get aiRecommendedSwitch => $_getN(4);\n  @$pb.TagNumber(5)\n  set aiRecommendedSwitch(PlayerDanmakuAiRecommendedSwitch v) {\n    setField(5, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasAiRecommendedSwitch() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearAiRecommendedSwitch() => clearField(5);\n  @$pb.TagNumber(5)\n  PlayerDanmakuAiRecommendedSwitch ensureAiRecommendedSwitch() => $_ensure(4);\n\n  @$pb.TagNumber(6)\n  PlayerDanmakuAiRecommendedLevel get aiRecommendedLevel => $_getN(5);\n  @$pb.TagNumber(6)\n  set aiRecommendedLevel(PlayerDanmakuAiRecommendedLevel v) {\n    setField(6, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasAiRecommendedLevel() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearAiRecommendedLevel() => clearField(6);\n  @$pb.TagNumber(6)\n  PlayerDanmakuAiRecommendedLevel ensureAiRecommendedLevel() => $_ensure(5);\n\n  @$pb.TagNumber(7)\n  PlayerDanmakuBlocktop get blocktop => $_getN(6);\n  @$pb.TagNumber(7)\n  set blocktop(PlayerDanmakuBlocktop v) {\n    setField(7, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasBlocktop() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearBlocktop() => clearField(7);\n  @$pb.TagNumber(7)\n  PlayerDanmakuBlocktop ensureBlocktop() => $_ensure(6);\n\n  @$pb.TagNumber(8)\n  PlayerDanmakuBlockscroll get blockscroll => $_getN(7);\n  @$pb.TagNumber(8)\n  set blockscroll(PlayerDanmakuBlockscroll v) {\n    setField(8, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasBlockscroll() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearBlockscroll() => clearField(8);\n  @$pb.TagNumber(8)\n  PlayerDanmakuBlockscroll ensureBlockscroll() => $_ensure(7);\n\n  @$pb.TagNumber(9)\n  PlayerDanmakuBlockbottom get blockbottom => $_getN(8);\n  @$pb.TagNumber(9)\n  set blockbottom(PlayerDanmakuBlockbottom v) {\n    setField(9, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasBlockbottom() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearBlockbottom() => clearField(9);\n  @$pb.TagNumber(9)\n  PlayerDanmakuBlockbottom ensureBlockbottom() => $_ensure(8);\n\n  @$pb.TagNumber(10)\n  PlayerDanmakuBlockcolorful get blockcolorful => $_getN(9);\n  @$pb.TagNumber(10)\n  set blockcolorful(PlayerDanmakuBlockcolorful v) {\n    setField(10, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasBlockcolorful() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearBlockcolorful() => clearField(10);\n  @$pb.TagNumber(10)\n  PlayerDanmakuBlockcolorful ensureBlockcolorful() => $_ensure(9);\n\n  @$pb.TagNumber(11)\n  PlayerDanmakuBlockrepeat get blockrepeat => $_getN(10);\n  @$pb.TagNumber(11)\n  set blockrepeat(PlayerDanmakuBlockrepeat v) {\n    setField(11, v);\n  }\n\n  @$pb.TagNumber(11)\n  $core.bool hasBlockrepeat() => $_has(10);\n  @$pb.TagNumber(11)\n  void clearBlockrepeat() => clearField(11);\n  @$pb.TagNumber(11)\n  PlayerDanmakuBlockrepeat ensureBlockrepeat() => $_ensure(10);\n\n  @$pb.TagNumber(12)\n  PlayerDanmakuBlockspecial get blockspecial => $_getN(11);\n  @$pb.TagNumber(12)\n  set blockspecial(PlayerDanmakuBlockspecial v) {\n    setField(12, v);\n  }\n\n  @$pb.TagNumber(12)\n  $core.bool hasBlockspecial() => $_has(11);\n  @$pb.TagNumber(12)\n  void clearBlockspecial() => clearField(12);\n  @$pb.TagNumber(12)\n  PlayerDanmakuBlockspecial ensureBlockspecial() => $_ensure(11);\n\n  @$pb.TagNumber(13)\n  PlayerDanmakuOpacity get opacity => $_getN(12);\n  @$pb.TagNumber(13)\n  set opacity(PlayerDanmakuOpacity v) {\n    setField(13, v);\n  }\n\n  @$pb.TagNumber(13)\n  $core.bool hasOpacity() => $_has(12);\n  @$pb.TagNumber(13)\n  void clearOpacity() => clearField(13);\n  @$pb.TagNumber(13)\n  PlayerDanmakuOpacity ensureOpacity() => $_ensure(12);\n\n  @$pb.TagNumber(14)\n  PlayerDanmakuScalingfactor get scalingfactor => $_getN(13);\n  @$pb.TagNumber(14)\n  set scalingfactor(PlayerDanmakuScalingfactor v) {\n    setField(14, v);\n  }\n\n  @$pb.TagNumber(14)\n  $core.bool hasScalingfactor() => $_has(13);\n  @$pb.TagNumber(14)\n  void clearScalingfactor() => clearField(14);\n  @$pb.TagNumber(14)\n  PlayerDanmakuScalingfactor ensureScalingfactor() => $_ensure(13);\n\n  @$pb.TagNumber(15)\n  PlayerDanmakuDomain get domain => $_getN(14);\n  @$pb.TagNumber(15)\n  set domain(PlayerDanmakuDomain v) {\n    setField(15, v);\n  }\n\n  @$pb.TagNumber(15)\n  $core.bool hasDomain() => $_has(14);\n  @$pb.TagNumber(15)\n  void clearDomain() => clearField(15);\n  @$pb.TagNumber(15)\n  PlayerDanmakuDomain ensureDomain() => $_ensure(14);\n\n  @$pb.TagNumber(16)\n  PlayerDanmakuSpeed get speed => $_getN(15);\n  @$pb.TagNumber(16)\n  set speed(PlayerDanmakuSpeed v) {\n    setField(16, v);\n  }\n\n  @$pb.TagNumber(16)\n  $core.bool hasSpeed() => $_has(15);\n  @$pb.TagNumber(16)\n  void clearSpeed() => clearField(16);\n  @$pb.TagNumber(16)\n  PlayerDanmakuSpeed ensureSpeed() => $_ensure(15);\n\n  @$pb.TagNumber(17)\n  PlayerDanmakuEnableblocklist get enableblocklist => $_getN(16);\n  @$pb.TagNumber(17)\n  set enableblocklist(PlayerDanmakuEnableblocklist v) {\n    setField(17, v);\n  }\n\n  @$pb.TagNumber(17)\n  $core.bool hasEnableblocklist() => $_has(16);\n  @$pb.TagNumber(17)\n  void clearEnableblocklist() => clearField(17);\n  @$pb.TagNumber(17)\n  PlayerDanmakuEnableblocklist ensureEnableblocklist() => $_ensure(16);\n\n  @$pb.TagNumber(18)\n  InlinePlayerDanmakuSwitch get inlinePlayerDanmakuSwitch => $_getN(17);\n  @$pb.TagNumber(18)\n  set inlinePlayerDanmakuSwitch(InlinePlayerDanmakuSwitch v) {\n    setField(18, v);\n  }\n\n  @$pb.TagNumber(18)\n  $core.bool hasInlinePlayerDanmakuSwitch() => $_has(17);\n  @$pb.TagNumber(18)\n  void clearInlinePlayerDanmakuSwitch() => clearField(18);\n  @$pb.TagNumber(18)\n  InlinePlayerDanmakuSwitch ensureInlinePlayerDanmakuSwitch() => $_ensure(17);\n\n  @$pb.TagNumber(19)\n  PlayerDanmakuSeniorModeSwitch get seniorModeSwitch => $_getN(18);\n  @$pb.TagNumber(19)\n  set seniorModeSwitch(PlayerDanmakuSeniorModeSwitch v) {\n    setField(19, v);\n  }\n\n  @$pb.TagNumber(19)\n  $core.bool hasSeniorModeSwitch() => $_has(18);\n  @$pb.TagNumber(19)\n  void clearSeniorModeSwitch() => clearField(19);\n  @$pb.TagNumber(19)\n  PlayerDanmakuSeniorModeSwitch ensureSeniorModeSwitch() => $_ensure(18);\n\n  @$pb.TagNumber(20)\n  PlayerDanmakuAiRecommendedLevelV2 get aiRecommendedLevelV2 => $_getN(19);\n  @$pb.TagNumber(20)\n  set aiRecommendedLevelV2(PlayerDanmakuAiRecommendedLevelV2 v) {\n    setField(20, v);\n  }\n\n  @$pb.TagNumber(20)\n  $core.bool hasAiRecommendedLevelV2() => $_has(19);\n  @$pb.TagNumber(20)\n  void clearAiRecommendedLevelV2() => clearField(20);\n  @$pb.TagNumber(20)\n  PlayerDanmakuAiRecommendedLevelV2 ensureAiRecommendedLevelV2() =>\n      $_ensure(19);\n}\n\nclass DmSegConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmSegConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'pageSize')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'total')\n    ..hasRequiredFields = false;\n\n  DmSegConfig._() : super();\n  factory DmSegConfig({\n    $fixnum.Int64? pageSize,\n    $fixnum.Int64? total,\n  }) {\n    final _result = create();\n    if (pageSize != null) {\n      _result.pageSize = pageSize;\n    }\n    if (total != null) {\n      _result.total = total;\n    }\n    return _result;\n  }\n  factory DmSegConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmSegConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmSegConfig clone() => DmSegConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmSegConfig copyWith(void Function(DmSegConfig) updates) =>\n      super.copyWith((message) => updates(message as DmSegConfig))\n          as DmSegConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmSegConfig create() => DmSegConfig._();\n  DmSegConfig createEmptyInstance() => create();\n  static $pb.PbList<DmSegConfig> createRepeated() => $pb.PbList<DmSegConfig>();\n  @$core.pragma('dart2js:noInline')\n  static DmSegConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmSegConfig>(create);\n  static DmSegConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get pageSize => $_getI64(0);\n  @$pb.TagNumber(1)\n  set pageSize($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasPageSize() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearPageSize() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get total => $_getI64(1);\n  @$pb.TagNumber(2)\n  set total($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasTotal() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearTotal() => clearField(2);\n}\n\nclass DmSegMobileReply extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmSegMobileReply',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pc<DanmakuElem>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'elems',\n        $pb.PbFieldType.PM,\n        subBuilder: DanmakuElem.create)\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'state',\n        $pb.PbFieldType.O3)\n    ..aOM<DanmakuAIFlag>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiFlag',\n        subBuilder: DanmakuAIFlag.create)\n    ..hasRequiredFields = false;\n\n  DmSegMobileReply._() : super();\n  factory DmSegMobileReply({\n    $core.Iterable<DanmakuElem>? elems,\n    $core.int? state,\n    DanmakuAIFlag? aiFlag,\n  }) {\n    final _result = create();\n    if (elems != null) {\n      _result.elems.addAll(elems);\n    }\n    if (state != null) {\n      _result.state = state;\n    }\n    if (aiFlag != null) {\n      _result.aiFlag = aiFlag;\n    }\n    return _result;\n  }\n  factory DmSegMobileReply.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmSegMobileReply.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmSegMobileReply clone() => DmSegMobileReply()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmSegMobileReply copyWith(void Function(DmSegMobileReply) updates) =>\n      super.copyWith((message) => updates(message as DmSegMobileReply))\n          as DmSegMobileReply; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmSegMobileReply create() => DmSegMobileReply._();\n  DmSegMobileReply createEmptyInstance() => create();\n  static $pb.PbList<DmSegMobileReply> createRepeated() =>\n      $pb.PbList<DmSegMobileReply>();\n  @$core.pragma('dart2js:noInline')\n  static DmSegMobileReply getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmSegMobileReply>(create);\n  static DmSegMobileReply? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<DanmakuElem> get elems => $_getList(0);\n\n  @$pb.TagNumber(2)\n  $core.int get state => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set state($core.int v) {\n    $_setSignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasState() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearState() => clearField(2);\n\n  @$pb.TagNumber(3)\n  DanmakuAIFlag get aiFlag => $_getN(2);\n  @$pb.TagNumber(3)\n  set aiFlag(DanmakuAIFlag v) {\n    setField(3, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasAiFlag() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearAiFlag() => clearField(3);\n  @$pb.TagNumber(3)\n  DanmakuAIFlag ensureAiFlag() => $_ensure(2);\n}\n\nclass DmSegMobileReq extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmSegMobileReq',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'pid')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'oid')\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'type',\n        $pb.PbFieldType.O3)\n    ..aInt64(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'segmentIndex')\n    ..a<$core.int>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'teenagersMode',\n        $pb.PbFieldType.O3)\n    ..aInt64(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'ps')\n    ..aInt64(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'pe')\n    ..a<$core.int>(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'pullMode',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'fromScene',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  DmSegMobileReq._() : super();\n  factory DmSegMobileReq({\n    $fixnum.Int64? pid,\n    $fixnum.Int64? oid,\n    $core.int? type,\n    $fixnum.Int64? segmentIndex,\n    $core.int? teenagersMode,\n    $fixnum.Int64? ps,\n    $fixnum.Int64? pe,\n    $core.int? pullMode,\n    $core.int? fromScene,\n  }) {\n    final _result = create();\n    if (pid != null) {\n      _result.pid = pid;\n    }\n    if (oid != null) {\n      _result.oid = oid;\n    }\n    if (type != null) {\n      _result.type = type;\n    }\n    if (segmentIndex != null) {\n      _result.segmentIndex = segmentIndex;\n    }\n    if (teenagersMode != null) {\n      _result.teenagersMode = teenagersMode;\n    }\n    if (ps != null) {\n      _result.ps = ps;\n    }\n    if (pe != null) {\n      _result.pe = pe;\n    }\n    if (pullMode != null) {\n      _result.pullMode = pullMode;\n    }\n    if (fromScene != null) {\n      _result.fromScene = fromScene;\n    }\n    return _result;\n  }\n  factory DmSegMobileReq.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmSegMobileReq.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmSegMobileReq clone() => DmSegMobileReq()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmSegMobileReq copyWith(void Function(DmSegMobileReq) updates) =>\n      super.copyWith((message) => updates(message as DmSegMobileReq))\n          as DmSegMobileReq; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmSegMobileReq create() => DmSegMobileReq._();\n  DmSegMobileReq createEmptyInstance() => create();\n  static $pb.PbList<DmSegMobileReq> createRepeated() =>\n      $pb.PbList<DmSegMobileReq>();\n  @$core.pragma('dart2js:noInline')\n  static DmSegMobileReq getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmSegMobileReq>(create);\n  static DmSegMobileReq? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get pid => $_getI64(0);\n  @$pb.TagNumber(1)\n  set pid($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasPid() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearPid() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get oid => $_getI64(1);\n  @$pb.TagNumber(2)\n  set oid($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasOid() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearOid() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get type => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set type($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearType() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $fixnum.Int64 get segmentIndex => $_getI64(3);\n  @$pb.TagNumber(4)\n  set segmentIndex($fixnum.Int64 v) {\n    $_setInt64(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasSegmentIndex() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearSegmentIndex() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.int get teenagersMode => $_getIZ(4);\n  @$pb.TagNumber(5)\n  set teenagersMode($core.int v) {\n    $_setSignedInt32(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasTeenagersMode() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearTeenagersMode() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $fixnum.Int64 get ps => $_getI64(5);\n  @$pb.TagNumber(6)\n  set ps($fixnum.Int64 v) {\n    $_setInt64(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasPs() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearPs() => clearField(6);\n\n  @$pb.TagNumber(7)\n  $fixnum.Int64 get pe => $_getI64(6);\n  @$pb.TagNumber(7)\n  set pe($fixnum.Int64 v) {\n    $_setInt64(6, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasPe() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearPe() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $core.int get pullMode => $_getIZ(7);\n  @$pb.TagNumber(8)\n  set pullMode($core.int v) {\n    $_setSignedInt32(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasPullMode() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearPullMode() => clearField(8);\n\n  @$pb.TagNumber(9)\n  $core.int get fromScene => $_getIZ(8);\n  @$pb.TagNumber(9)\n  set fromScene($core.int v) {\n    $_setSignedInt32(8, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasFromScene() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearFromScene() => clearField(9);\n}\n\nclass DmSegOttReply extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmSegOttReply',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'closed')\n    ..pc<DanmakuElem>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'elems',\n        $pb.PbFieldType.PM,\n        subBuilder: DanmakuElem.create)\n    ..hasRequiredFields = false;\n\n  DmSegOttReply._() : super();\n  factory DmSegOttReply({\n    $core.bool? closed,\n    $core.Iterable<DanmakuElem>? elems,\n  }) {\n    final _result = create();\n    if (closed != null) {\n      _result.closed = closed;\n    }\n    if (elems != null) {\n      _result.elems.addAll(elems);\n    }\n    return _result;\n  }\n  factory DmSegOttReply.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmSegOttReply.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmSegOttReply clone() => DmSegOttReply()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmSegOttReply copyWith(void Function(DmSegOttReply) updates) =>\n      super.copyWith((message) => updates(message as DmSegOttReply))\n          as DmSegOttReply; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmSegOttReply create() => DmSegOttReply._();\n  DmSegOttReply createEmptyInstance() => create();\n  static $pb.PbList<DmSegOttReply> createRepeated() =>\n      $pb.PbList<DmSegOttReply>();\n  @$core.pragma('dart2js:noInline')\n  static DmSegOttReply getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmSegOttReply>(create);\n  static DmSegOttReply? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get closed => $_getBF(0);\n  @$pb.TagNumber(1)\n  set closed($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasClosed() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearClosed() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.List<DanmakuElem> get elems => $_getList(1);\n}\n\nclass DmSegOttReq extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmSegOttReq',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'pid')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'oid')\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'type',\n        $pb.PbFieldType.O3)\n    ..aInt64(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'segmentIndex')\n    ..hasRequiredFields = false;\n\n  DmSegOttReq._() : super();\n  factory DmSegOttReq({\n    $fixnum.Int64? pid,\n    $fixnum.Int64? oid,\n    $core.int? type,\n    $fixnum.Int64? segmentIndex,\n  }) {\n    final _result = create();\n    if (pid != null) {\n      _result.pid = pid;\n    }\n    if (oid != null) {\n      _result.oid = oid;\n    }\n    if (type != null) {\n      _result.type = type;\n    }\n    if (segmentIndex != null) {\n      _result.segmentIndex = segmentIndex;\n    }\n    return _result;\n  }\n  factory DmSegOttReq.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmSegOttReq.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmSegOttReq clone() => DmSegOttReq()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmSegOttReq copyWith(void Function(DmSegOttReq) updates) =>\n      super.copyWith((message) => updates(message as DmSegOttReq))\n          as DmSegOttReq; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmSegOttReq create() => DmSegOttReq._();\n  DmSegOttReq createEmptyInstance() => create();\n  static $pb.PbList<DmSegOttReq> createRepeated() => $pb.PbList<DmSegOttReq>();\n  @$core.pragma('dart2js:noInline')\n  static DmSegOttReq getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmSegOttReq>(create);\n  static DmSegOttReq? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get pid => $_getI64(0);\n  @$pb.TagNumber(1)\n  set pid($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasPid() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearPid() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get oid => $_getI64(1);\n  @$pb.TagNumber(2)\n  set oid($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasOid() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearOid() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get type => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set type($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearType() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $fixnum.Int64 get segmentIndex => $_getI64(3);\n  @$pb.TagNumber(4)\n  set segmentIndex($fixnum.Int64 v) {\n    $_setInt64(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasSegmentIndex() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearSegmentIndex() => clearField(4);\n}\n\nclass DmSegSDKReply extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmSegSDKReply',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'closed')\n    ..pc<DanmakuElem>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'elems',\n        $pb.PbFieldType.PM,\n        subBuilder: DanmakuElem.create)\n    ..hasRequiredFields = false;\n\n  DmSegSDKReply._() : super();\n  factory DmSegSDKReply({\n    $core.bool? closed,\n    $core.Iterable<DanmakuElem>? elems,\n  }) {\n    final _result = create();\n    if (closed != null) {\n      _result.closed = closed;\n    }\n    if (elems != null) {\n      _result.elems.addAll(elems);\n    }\n    return _result;\n  }\n  factory DmSegSDKReply.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmSegSDKReply.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmSegSDKReply clone() => DmSegSDKReply()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmSegSDKReply copyWith(void Function(DmSegSDKReply) updates) =>\n      super.copyWith((message) => updates(message as DmSegSDKReply))\n          as DmSegSDKReply; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmSegSDKReply create() => DmSegSDKReply._();\n  DmSegSDKReply createEmptyInstance() => create();\n  static $pb.PbList<DmSegSDKReply> createRepeated() =>\n      $pb.PbList<DmSegSDKReply>();\n  @$core.pragma('dart2js:noInline')\n  static DmSegSDKReply getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmSegSDKReply>(create);\n  static DmSegSDKReply? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get closed => $_getBF(0);\n  @$pb.TagNumber(1)\n  set closed($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasClosed() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearClosed() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.List<DanmakuElem> get elems => $_getList(1);\n}\n\nclass DmSegSDKReq extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmSegSDKReq',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'pid')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'oid')\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'type',\n        $pb.PbFieldType.O3)\n    ..aInt64(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'segmentIndex')\n    ..hasRequiredFields = false;\n\n  DmSegSDKReq._() : super();\n  factory DmSegSDKReq({\n    $fixnum.Int64? pid,\n    $fixnum.Int64? oid,\n    $core.int? type,\n    $fixnum.Int64? segmentIndex,\n  }) {\n    final _result = create();\n    if (pid != null) {\n      _result.pid = pid;\n    }\n    if (oid != null) {\n      _result.oid = oid;\n    }\n    if (type != null) {\n      _result.type = type;\n    }\n    if (segmentIndex != null) {\n      _result.segmentIndex = segmentIndex;\n    }\n    return _result;\n  }\n  factory DmSegSDKReq.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmSegSDKReq.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmSegSDKReq clone() => DmSegSDKReq()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmSegSDKReq copyWith(void Function(DmSegSDKReq) updates) =>\n      super.copyWith((message) => updates(message as DmSegSDKReq))\n          as DmSegSDKReq; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmSegSDKReq create() => DmSegSDKReq._();\n  DmSegSDKReq createEmptyInstance() => create();\n  static $pb.PbList<DmSegSDKReq> createRepeated() => $pb.PbList<DmSegSDKReq>();\n  @$core.pragma('dart2js:noInline')\n  static DmSegSDKReq getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmSegSDKReq>(create);\n  static DmSegSDKReq? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get pid => $_getI64(0);\n  @$pb.TagNumber(1)\n  set pid($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasPid() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearPid() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get oid => $_getI64(1);\n  @$pb.TagNumber(2)\n  set oid($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasOid() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearOid() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get type => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set type($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearType() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $fixnum.Int64 get segmentIndex => $_getI64(3);\n  @$pb.TagNumber(4)\n  set segmentIndex($fixnum.Int64 v) {\n    $_setInt64(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasSegmentIndex() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearSegmentIndex() => clearField(4);\n}\n\nclass DmViewReply extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmViewReply',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'closed')\n    ..aOM<VideoMask>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'mask',\n        subBuilder: VideoMask.create)\n    ..aOM<VideoSubtitle>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'subtitle',\n        subBuilder: VideoSubtitle.create)\n    ..pPS(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'specialDms')\n    ..aOM<DanmakuFlagConfig>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiFlag',\n        subBuilder: DanmakuFlagConfig.create)\n    ..aOM<DanmuPlayerViewConfig>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerConfig',\n        subBuilder: DanmuPlayerViewConfig.create)\n    ..a<$core.int>(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'sendBoxStyle',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'allow')\n    ..aOS(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'checkBox')\n    ..aOS(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'checkBoxShowMsg')\n    ..aOS(\n        11,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'textPlaceholder')\n    ..aOS(\n        12,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'inputPlaceholder')\n    ..pPS(\n        13,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'reportFilterContent')\n    ..aOM<ExpoReport>(\n        14,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'expoReport',\n        subBuilder: ExpoReport.create)\n    ..aOM<BuzzwordConfig>(\n        15,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'buzzwordConfig',\n        subBuilder: BuzzwordConfig.create)\n    ..pc<Expressions>(\n        16,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'expressions',\n        $pb.PbFieldType.PM,\n        subBuilder: Expressions.create)\n    ..pc<PostPanel>(\n        17,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'postPanel',\n        $pb.PbFieldType.PM,\n        subBuilder: PostPanel.create)\n    ..pPS(\n        18,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'activityMeta')\n    ..pc<PostPanelV2>(\n        19,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'postPanel2',\n        $pb.PbFieldType.PM,\n        subBuilder: PostPanelV2.create)\n    ..hasRequiredFields = false;\n\n  DmViewReply._() : super();\n  factory DmViewReply({\n    $core.bool? closed,\n    VideoMask? mask,\n    VideoSubtitle? subtitle,\n    $core.Iterable<$core.String>? specialDms,\n    DanmakuFlagConfig? aiFlag,\n    DanmuPlayerViewConfig? playerConfig,\n    $core.int? sendBoxStyle,\n    $core.bool? allow,\n    $core.String? checkBox,\n    $core.String? checkBoxShowMsg,\n    $core.String? textPlaceholder,\n    $core.String? inputPlaceholder,\n    $core.Iterable<$core.String>? reportFilterContent,\n    ExpoReport? expoReport,\n    BuzzwordConfig? buzzwordConfig,\n    $core.Iterable<Expressions>? expressions,\n    $core.Iterable<PostPanel>? postPanel,\n    $core.Iterable<$core.String>? activityMeta,\n    $core.Iterable<PostPanelV2>? postPanel2,\n  }) {\n    final _result = create();\n    if (closed != null) {\n      _result.closed = closed;\n    }\n    if (mask != null) {\n      _result.mask = mask;\n    }\n    if (subtitle != null) {\n      _result.subtitle = subtitle;\n    }\n    if (specialDms != null) {\n      _result.specialDms.addAll(specialDms);\n    }\n    if (aiFlag != null) {\n      _result.aiFlag = aiFlag;\n    }\n    if (playerConfig != null) {\n      _result.playerConfig = playerConfig;\n    }\n    if (sendBoxStyle != null) {\n      _result.sendBoxStyle = sendBoxStyle;\n    }\n    if (allow != null) {\n      _result.allow = allow;\n    }\n    if (checkBox != null) {\n      _result.checkBox = checkBox;\n    }\n    if (checkBoxShowMsg != null) {\n      _result.checkBoxShowMsg = checkBoxShowMsg;\n    }\n    if (textPlaceholder != null) {\n      _result.textPlaceholder = textPlaceholder;\n    }\n    if (inputPlaceholder != null) {\n      _result.inputPlaceholder = inputPlaceholder;\n    }\n    if (reportFilterContent != null) {\n      _result.reportFilterContent.addAll(reportFilterContent);\n    }\n    if (expoReport != null) {\n      _result.expoReport = expoReport;\n    }\n    if (buzzwordConfig != null) {\n      _result.buzzwordConfig = buzzwordConfig;\n    }\n    if (expressions != null) {\n      _result.expressions.addAll(expressions);\n    }\n    if (postPanel != null) {\n      _result.postPanel.addAll(postPanel);\n    }\n    if (activityMeta != null) {\n      _result.activityMeta.addAll(activityMeta);\n    }\n    if (postPanel2 != null) {\n      _result.postPanel2.addAll(postPanel2);\n    }\n    return _result;\n  }\n  factory DmViewReply.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmViewReply.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmViewReply clone() => DmViewReply()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmViewReply copyWith(void Function(DmViewReply) updates) =>\n      super.copyWith((message) => updates(message as DmViewReply))\n          as DmViewReply; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmViewReply create() => DmViewReply._();\n  DmViewReply createEmptyInstance() => create();\n  static $pb.PbList<DmViewReply> createRepeated() => $pb.PbList<DmViewReply>();\n  @$core.pragma('dart2js:noInline')\n  static DmViewReply getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmViewReply>(create);\n  static DmViewReply? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get closed => $_getBF(0);\n  @$pb.TagNumber(1)\n  set closed($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasClosed() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearClosed() => clearField(1);\n\n  @$pb.TagNumber(2)\n  VideoMask get mask => $_getN(1);\n  @$pb.TagNumber(2)\n  set mask(VideoMask v) {\n    setField(2, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasMask() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearMask() => clearField(2);\n  @$pb.TagNumber(2)\n  VideoMask ensureMask() => $_ensure(1);\n\n  @$pb.TagNumber(3)\n  VideoSubtitle get subtitle => $_getN(2);\n  @$pb.TagNumber(3)\n  set subtitle(VideoSubtitle v) {\n    setField(3, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasSubtitle() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearSubtitle() => clearField(3);\n  @$pb.TagNumber(3)\n  VideoSubtitle ensureSubtitle() => $_ensure(2);\n\n  @$pb.TagNumber(4)\n  $core.List<$core.String> get specialDms => $_getList(3);\n\n  @$pb.TagNumber(5)\n  DanmakuFlagConfig get aiFlag => $_getN(4);\n  @$pb.TagNumber(5)\n  set aiFlag(DanmakuFlagConfig v) {\n    setField(5, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasAiFlag() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearAiFlag() => clearField(5);\n  @$pb.TagNumber(5)\n  DanmakuFlagConfig ensureAiFlag() => $_ensure(4);\n\n  @$pb.TagNumber(6)\n  DanmuPlayerViewConfig get playerConfig => $_getN(5);\n  @$pb.TagNumber(6)\n  set playerConfig(DanmuPlayerViewConfig v) {\n    setField(6, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasPlayerConfig() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearPlayerConfig() => clearField(6);\n  @$pb.TagNumber(6)\n  DanmuPlayerViewConfig ensurePlayerConfig() => $_ensure(5);\n\n  @$pb.TagNumber(7)\n  $core.int get sendBoxStyle => $_getIZ(6);\n  @$pb.TagNumber(7)\n  set sendBoxStyle($core.int v) {\n    $_setSignedInt32(6, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasSendBoxStyle() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearSendBoxStyle() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $core.bool get allow => $_getBF(7);\n  @$pb.TagNumber(8)\n  set allow($core.bool v) {\n    $_setBool(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasAllow() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearAllow() => clearField(8);\n\n  @$pb.TagNumber(9)\n  $core.String get checkBox => $_getSZ(8);\n  @$pb.TagNumber(9)\n  set checkBox($core.String v) {\n    $_setString(8, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasCheckBox() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearCheckBox() => clearField(9);\n\n  @$pb.TagNumber(10)\n  $core.String get checkBoxShowMsg => $_getSZ(9);\n  @$pb.TagNumber(10)\n  set checkBoxShowMsg($core.String v) {\n    $_setString(9, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasCheckBoxShowMsg() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearCheckBoxShowMsg() => clearField(10);\n\n  @$pb.TagNumber(11)\n  $core.String get textPlaceholder => $_getSZ(10);\n  @$pb.TagNumber(11)\n  set textPlaceholder($core.String v) {\n    $_setString(10, v);\n  }\n\n  @$pb.TagNumber(11)\n  $core.bool hasTextPlaceholder() => $_has(10);\n  @$pb.TagNumber(11)\n  void clearTextPlaceholder() => clearField(11);\n\n  @$pb.TagNumber(12)\n  $core.String get inputPlaceholder => $_getSZ(11);\n  @$pb.TagNumber(12)\n  set inputPlaceholder($core.String v) {\n    $_setString(11, v);\n  }\n\n  @$pb.TagNumber(12)\n  $core.bool hasInputPlaceholder() => $_has(11);\n  @$pb.TagNumber(12)\n  void clearInputPlaceholder() => clearField(12);\n\n  @$pb.TagNumber(13)\n  $core.List<$core.String> get reportFilterContent => $_getList(12);\n\n  @$pb.TagNumber(14)\n  ExpoReport get expoReport => $_getN(13);\n  @$pb.TagNumber(14)\n  set expoReport(ExpoReport v) {\n    setField(14, v);\n  }\n\n  @$pb.TagNumber(14)\n  $core.bool hasExpoReport() => $_has(13);\n  @$pb.TagNumber(14)\n  void clearExpoReport() => clearField(14);\n  @$pb.TagNumber(14)\n  ExpoReport ensureExpoReport() => $_ensure(13);\n\n  @$pb.TagNumber(15)\n  BuzzwordConfig get buzzwordConfig => $_getN(14);\n  @$pb.TagNumber(15)\n  set buzzwordConfig(BuzzwordConfig v) {\n    setField(15, v);\n  }\n\n  @$pb.TagNumber(15)\n  $core.bool hasBuzzwordConfig() => $_has(14);\n  @$pb.TagNumber(15)\n  void clearBuzzwordConfig() => clearField(15);\n  @$pb.TagNumber(15)\n  BuzzwordConfig ensureBuzzwordConfig() => $_ensure(14);\n\n  @$pb.TagNumber(16)\n  $core.List<Expressions> get expressions => $_getList(15);\n\n  @$pb.TagNumber(17)\n  $core.List<PostPanel> get postPanel => $_getList(16);\n\n  @$pb.TagNumber(18)\n  $core.List<$core.String> get activityMeta => $_getList(17);\n\n  @$pb.TagNumber(19)\n  $core.List<PostPanelV2> get postPanel2 => $_getList(18);\n}\n\nclass DmViewReq extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmViewReq',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'pid')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'oid')\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'type',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'spmid')\n    ..a<$core.int>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'isHardBoot',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  DmViewReq._() : super();\n  factory DmViewReq({\n    $fixnum.Int64? pid,\n    $fixnum.Int64? oid,\n    $core.int? type,\n    $core.String? spmid,\n    $core.int? isHardBoot,\n  }) {\n    final _result = create();\n    if (pid != null) {\n      _result.pid = pid;\n    }\n    if (oid != null) {\n      _result.oid = oid;\n    }\n    if (type != null) {\n      _result.type = type;\n    }\n    if (spmid != null) {\n      _result.spmid = spmid;\n    }\n    if (isHardBoot != null) {\n      _result.isHardBoot = isHardBoot;\n    }\n    return _result;\n  }\n  factory DmViewReq.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmViewReq.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmViewReq clone() => DmViewReq()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmViewReq copyWith(void Function(DmViewReq) updates) =>\n      super.copyWith((message) => updates(message as DmViewReq))\n          as DmViewReq; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmViewReq create() => DmViewReq._();\n  DmViewReq createEmptyInstance() => create();\n  static $pb.PbList<DmViewReq> createRepeated() => $pb.PbList<DmViewReq>();\n  @$core.pragma('dart2js:noInline')\n  static DmViewReq getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DmViewReq>(create);\n  static DmViewReq? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get pid => $_getI64(0);\n  @$pb.TagNumber(1)\n  set pid($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasPid() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearPid() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get oid => $_getI64(1);\n  @$pb.TagNumber(2)\n  set oid($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasOid() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearOid() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get type => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set type($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearType() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.String get spmid => $_getSZ(3);\n  @$pb.TagNumber(4)\n  set spmid($core.String v) {\n    $_setString(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasSpmid() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearSpmid() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.int get isHardBoot => $_getIZ(4);\n  @$pb.TagNumber(5)\n  set isHardBoot($core.int v) {\n    $_setSignedInt32(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasIsHardBoot() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearIsHardBoot() => clearField(5);\n}\n\nclass DmWebViewReply extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'DmWebViewReply',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.int>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'state',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..aOS(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'textSide')\n    ..aOM<DmSegConfig>(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'dmSge',\n        subBuilder: DmSegConfig.create)\n    ..aOM<DanmakuFlagConfig>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'flag',\n        subBuilder: DanmakuFlagConfig.create)\n    ..pPS(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'specialDms')\n    ..aOB(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'checkBox')\n    ..aInt64(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'count')\n    ..pc<CommandDm>(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'commandDms',\n        $pb.PbFieldType.PM,\n        protoName: 'commandDms',\n        subBuilder: CommandDm.create)\n    ..aOM<DanmuWebPlayerConfig>(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'playerConfig',\n        subBuilder: DanmuWebPlayerConfig.create)\n    ..pPS(\n        11,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'reportFilterContent')\n    ..pc<Expressions>(\n        12,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'expressions',\n        $pb.PbFieldType.PM,\n        subBuilder: Expressions.create)\n    ..pc<PostPanel>(\n        13,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'postPanel',\n        $pb.PbFieldType.PM,\n        subBuilder: PostPanel.create)\n    ..pPS(\n        14,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'activityMeta')\n    ..hasRequiredFields = false;\n\n  DmWebViewReply._() : super();\n  factory DmWebViewReply({\n    $core.int? state,\n    $core.String? text,\n    $core.String? textSide,\n    DmSegConfig? dmSge,\n    DanmakuFlagConfig? flag,\n    $core.Iterable<$core.String>? specialDms,\n    $core.bool? checkBox,\n    $fixnum.Int64? count,\n    $core.Iterable<CommandDm>? commandDms,\n    DanmuWebPlayerConfig? playerConfig,\n    $core.Iterable<$core.String>? reportFilterContent,\n    $core.Iterable<Expressions>? expressions,\n    $core.Iterable<PostPanel>? postPanel,\n    $core.Iterable<$core.String>? activityMeta,\n  }) {\n    final _result = create();\n    if (state != null) {\n      _result.state = state;\n    }\n    if (text != null) {\n      _result.text = text;\n    }\n    if (textSide != null) {\n      _result.textSide = textSide;\n    }\n    if (dmSge != null) {\n      _result.dmSge = dmSge;\n    }\n    if (flag != null) {\n      _result.flag = flag;\n    }\n    if (specialDms != null) {\n      _result.specialDms.addAll(specialDms);\n    }\n    if (checkBox != null) {\n      _result.checkBox = checkBox;\n    }\n    if (count != null) {\n      _result.count = count;\n    }\n    if (commandDms != null) {\n      _result.commandDms.addAll(commandDms);\n    }\n    if (playerConfig != null) {\n      _result.playerConfig = playerConfig;\n    }\n    if (reportFilterContent != null) {\n      _result.reportFilterContent.addAll(reportFilterContent);\n    }\n    if (expressions != null) {\n      _result.expressions.addAll(expressions);\n    }\n    if (postPanel != null) {\n      _result.postPanel.addAll(postPanel);\n    }\n    if (activityMeta != null) {\n      _result.activityMeta.addAll(activityMeta);\n    }\n    return _result;\n  }\n  factory DmWebViewReply.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory DmWebViewReply.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  DmWebViewReply clone() => DmWebViewReply()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  DmWebViewReply copyWith(void Function(DmWebViewReply) updates) =>\n      super.copyWith((message) => updates(message as DmWebViewReply))\n          as DmWebViewReply; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static DmWebViewReply create() => DmWebViewReply._();\n  DmWebViewReply createEmptyInstance() => create();\n  static $pb.PbList<DmWebViewReply> createRepeated() =>\n      $pb.PbList<DmWebViewReply>();\n  @$core.pragma('dart2js:noInline')\n  static DmWebViewReply getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<DmWebViewReply>(create);\n  static DmWebViewReply? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.int get state => $_getIZ(0);\n  @$pb.TagNumber(1)\n  set state($core.int v) {\n    $_setSignedInt32(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasState() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearState() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get text => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set text($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasText() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearText() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.String get textSide => $_getSZ(2);\n  @$pb.TagNumber(3)\n  set textSide($core.String v) {\n    $_setString(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasTextSide() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearTextSide() => clearField(3);\n\n  @$pb.TagNumber(4)\n  DmSegConfig get dmSge => $_getN(3);\n  @$pb.TagNumber(4)\n  set dmSge(DmSegConfig v) {\n    setField(4, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasDmSge() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearDmSge() => clearField(4);\n  @$pb.TagNumber(4)\n  DmSegConfig ensureDmSge() => $_ensure(3);\n\n  @$pb.TagNumber(5)\n  DanmakuFlagConfig get flag => $_getN(4);\n  @$pb.TagNumber(5)\n  set flag(DanmakuFlagConfig v) {\n    setField(5, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasFlag() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearFlag() => clearField(5);\n  @$pb.TagNumber(5)\n  DanmakuFlagConfig ensureFlag() => $_ensure(4);\n\n  @$pb.TagNumber(6)\n  $core.List<$core.String> get specialDms => $_getList(5);\n\n  @$pb.TagNumber(7)\n  $core.bool get checkBox => $_getBF(6);\n  @$pb.TagNumber(7)\n  set checkBox($core.bool v) {\n    $_setBool(6, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasCheckBox() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearCheckBox() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $fixnum.Int64 get count => $_getI64(7);\n  @$pb.TagNumber(8)\n  set count($fixnum.Int64 v) {\n    $_setInt64(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasCount() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearCount() => clearField(8);\n\n  @$pb.TagNumber(9)\n  $core.List<CommandDm> get commandDms => $_getList(8);\n\n  @$pb.TagNumber(10)\n  DanmuWebPlayerConfig get playerConfig => $_getN(9);\n  @$pb.TagNumber(10)\n  set playerConfig(DanmuWebPlayerConfig v) {\n    setField(10, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasPlayerConfig() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearPlayerConfig() => clearField(10);\n  @$pb.TagNumber(10)\n  DanmuWebPlayerConfig ensurePlayerConfig() => $_ensure(9);\n\n  @$pb.TagNumber(11)\n  $core.List<$core.String> get reportFilterContent => $_getList(10);\n\n  @$pb.TagNumber(12)\n  $core.List<Expressions> get expressions => $_getList(11);\n\n  @$pb.TagNumber(13)\n  $core.List<PostPanel> get postPanel => $_getList(12);\n\n  @$pb.TagNumber(14)\n  $core.List<$core.String> get activityMeta => $_getList(13);\n}\n\nclass ExpoReport extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'ExpoReport',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'shouldReportAtEnd')\n    ..hasRequiredFields = false;\n\n  ExpoReport._() : super();\n  factory ExpoReport({\n    $core.bool? shouldReportAtEnd,\n  }) {\n    final _result = create();\n    if (shouldReportAtEnd != null) {\n      _result.shouldReportAtEnd = shouldReportAtEnd;\n    }\n    return _result;\n  }\n  factory ExpoReport.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory ExpoReport.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  ExpoReport clone() => ExpoReport()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  ExpoReport copyWith(void Function(ExpoReport) updates) =>\n      super.copyWith((message) => updates(message as ExpoReport))\n          as ExpoReport; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static ExpoReport create() => ExpoReport._();\n  ExpoReport createEmptyInstance() => create();\n  static $pb.PbList<ExpoReport> createRepeated() => $pb.PbList<ExpoReport>();\n  @$core.pragma('dart2js:noInline')\n  static ExpoReport getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<ExpoReport>(create);\n  static ExpoReport? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get shouldReportAtEnd => $_getBF(0);\n  @$pb.TagNumber(1)\n  set shouldReportAtEnd($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasShouldReportAtEnd() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearShouldReportAtEnd() => clearField(1);\n}\n\nclass Expression extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Expression',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pPS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'keyword')\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'url')\n    ..pc<Period>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'period',\n        $pb.PbFieldType.PM,\n        subBuilder: Period.create)\n    ..hasRequiredFields = false;\n\n  Expression._() : super();\n  factory Expression({\n    $core.Iterable<$core.String>? keyword,\n    $core.String? url,\n    $core.Iterable<Period>? period,\n  }) {\n    final _result = create();\n    if (keyword != null) {\n      _result.keyword.addAll(keyword);\n    }\n    if (url != null) {\n      _result.url = url;\n    }\n    if (period != null) {\n      _result.period.addAll(period);\n    }\n    return _result;\n  }\n  factory Expression.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Expression.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Expression clone() => Expression()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Expression copyWith(void Function(Expression) updates) =>\n      super.copyWith((message) => updates(message as Expression))\n          as Expression; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Expression create() => Expression._();\n  Expression createEmptyInstance() => create();\n  static $pb.PbList<Expression> createRepeated() => $pb.PbList<Expression>();\n  @$core.pragma('dart2js:noInline')\n  static Expression getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<Expression>(create);\n  static Expression? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<$core.String> get keyword => $_getList(0);\n\n  @$pb.TagNumber(2)\n  $core.String get url => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set url($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasUrl() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearUrl() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.List<Period> get period => $_getList(2);\n}\n\nclass Expressions extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Expressions',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pc<Expression>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'data',\n        $pb.PbFieldType.PM,\n        subBuilder: Expression.create)\n    ..hasRequiredFields = false;\n\n  Expressions._() : super();\n  factory Expressions({\n    $core.Iterable<Expression>? data,\n  }) {\n    final _result = create();\n    if (data != null) {\n      _result.data.addAll(data);\n    }\n    return _result;\n  }\n  factory Expressions.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Expressions.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Expressions clone() => Expressions()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Expressions copyWith(void Function(Expressions) updates) =>\n      super.copyWith((message) => updates(message as Expressions))\n          as Expressions; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Expressions create() => Expressions._();\n  Expressions createEmptyInstance() => create();\n  static $pb.PbList<Expressions> createRepeated() => $pb.PbList<Expressions>();\n  @$core.pragma('dart2js:noInline')\n  static Expressions getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<Expressions>(create);\n  static Expressions? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<Expression> get data => $_getList(0);\n}\n\nclass InlinePlayerDanmakuSwitch extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'InlinePlayerDanmakuSwitch',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  InlinePlayerDanmakuSwitch._() : super();\n  factory InlinePlayerDanmakuSwitch({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory InlinePlayerDanmakuSwitch.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory InlinePlayerDanmakuSwitch.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  InlinePlayerDanmakuSwitch clone() =>\n      InlinePlayerDanmakuSwitch()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  InlinePlayerDanmakuSwitch copyWith(\n          void Function(InlinePlayerDanmakuSwitch) updates) =>\n      super.copyWith((message) => updates(message as InlinePlayerDanmakuSwitch))\n          as InlinePlayerDanmakuSwitch; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static InlinePlayerDanmakuSwitch create() => InlinePlayerDanmakuSwitch._();\n  InlinePlayerDanmakuSwitch createEmptyInstance() => create();\n  static $pb.PbList<InlinePlayerDanmakuSwitch> createRepeated() =>\n      $pb.PbList<InlinePlayerDanmakuSwitch>();\n  @$core.pragma('dart2js:noInline')\n  static InlinePlayerDanmakuSwitch getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<InlinePlayerDanmakuSwitch>(create);\n  static InlinePlayerDanmakuSwitch? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass Label extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Label',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'title')\n    ..pPS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'content')\n    ..hasRequiredFields = false;\n\n  Label._() : super();\n  factory Label({\n    $core.String? title,\n    $core.Iterable<$core.String>? content,\n  }) {\n    final _result = create();\n    if (title != null) {\n      _result.title = title;\n    }\n    if (content != null) {\n      _result.content.addAll(content);\n    }\n    return _result;\n  }\n  factory Label.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Label.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Label clone() => Label()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Label copyWith(void Function(Label) updates) =>\n      super.copyWith((message) => updates(message as Label))\n          as Label; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Label create() => Label._();\n  Label createEmptyInstance() => create();\n  static $pb.PbList<Label> createRepeated() => $pb.PbList<Label>();\n  @$core.pragma('dart2js:noInline')\n  static Label getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Label>(create);\n  static Label? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get title => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set title($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasTitle() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearTitle() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.List<$core.String> get content => $_getList(1);\n}\n\nclass LabelV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'LabelV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'title')\n    ..pPS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'content')\n    ..aOB(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'exposureOnce')\n    ..a<$core.int>(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'exposureType',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  LabelV2._() : super();\n  factory LabelV2({\n    $core.String? title,\n    $core.Iterable<$core.String>? content,\n    $core.bool? exposureOnce,\n    $core.int? exposureType,\n  }) {\n    final _result = create();\n    if (title != null) {\n      _result.title = title;\n    }\n    if (content != null) {\n      _result.content.addAll(content);\n    }\n    if (exposureOnce != null) {\n      _result.exposureOnce = exposureOnce;\n    }\n    if (exposureType != null) {\n      _result.exposureType = exposureType;\n    }\n    return _result;\n  }\n  factory LabelV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory LabelV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  LabelV2 clone() => LabelV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  LabelV2 copyWith(void Function(LabelV2) updates) =>\n      super.copyWith((message) => updates(message as LabelV2))\n          as LabelV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static LabelV2 create() => LabelV2._();\n  LabelV2 createEmptyInstance() => create();\n  static $pb.PbList<LabelV2> createRepeated() => $pb.PbList<LabelV2>();\n  @$core.pragma('dart2js:noInline')\n  static LabelV2 getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<LabelV2>(create);\n  static LabelV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get title => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set title($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasTitle() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearTitle() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.List<$core.String> get content => $_getList(1);\n\n  @$pb.TagNumber(3)\n  $core.bool get exposureOnce => $_getBF(2);\n  @$pb.TagNumber(3)\n  set exposureOnce($core.bool v) {\n    $_setBool(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasExposureOnce() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearExposureOnce() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.int get exposureType => $_getIZ(3);\n  @$pb.TagNumber(4)\n  set exposureType($core.int v) {\n    $_setSignedInt32(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasExposureType() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearExposureType() => clearField(4);\n}\n\nclass Period extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Period',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'start')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'end')\n    ..hasRequiredFields = false;\n\n  Period._() : super();\n  factory Period({\n    $fixnum.Int64? start,\n    $fixnum.Int64? end,\n  }) {\n    final _result = create();\n    if (start != null) {\n      _result.start = start;\n    }\n    if (end != null) {\n      _result.end = end;\n    }\n    return _result;\n  }\n  factory Period.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Period.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Period clone() => Period()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Period copyWith(void Function(Period) updates) =>\n      super.copyWith((message) => updates(message as Period))\n          as Period; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Period create() => Period._();\n  Period createEmptyInstance() => create();\n  static $pb.PbList<Period> createRepeated() => $pb.PbList<Period>();\n  @$core.pragma('dart2js:noInline')\n  static Period getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Period>(create);\n  static Period? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get start => $_getI64(0);\n  @$pb.TagNumber(1)\n  set start($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasStart() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearStart() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get end => $_getI64(1);\n  @$pb.TagNumber(2)\n  set end($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasEnd() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearEnd() => clearField(2);\n}\n\nclass PlayerDanmakuAiRecommendedLevel extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuAiRecommendedLevel',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuAiRecommendedLevel._() : super();\n  factory PlayerDanmakuAiRecommendedLevel({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuAiRecommendedLevel.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuAiRecommendedLevel.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuAiRecommendedLevel clone() =>\n      PlayerDanmakuAiRecommendedLevel()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuAiRecommendedLevel copyWith(\n          void Function(PlayerDanmakuAiRecommendedLevel) updates) =>\n      super.copyWith(\n              (message) => updates(message as PlayerDanmakuAiRecommendedLevel))\n          as PlayerDanmakuAiRecommendedLevel; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuAiRecommendedLevel create() =>\n      PlayerDanmakuAiRecommendedLevel._();\n  PlayerDanmakuAiRecommendedLevel createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuAiRecommendedLevel> createRepeated() =>\n      $pb.PbList<PlayerDanmakuAiRecommendedLevel>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuAiRecommendedLevel getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuAiRecommendedLevel>(\n          create);\n  static PlayerDanmakuAiRecommendedLevel? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuAiRecommendedLevelV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuAiRecommendedLevelV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.int>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuAiRecommendedLevelV2._() : super();\n  factory PlayerDanmakuAiRecommendedLevelV2({\n    $core.int? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuAiRecommendedLevelV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuAiRecommendedLevelV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuAiRecommendedLevelV2 clone() =>\n      PlayerDanmakuAiRecommendedLevelV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuAiRecommendedLevelV2 copyWith(\n          void Function(PlayerDanmakuAiRecommendedLevelV2) updates) =>\n      super.copyWith((message) =>\n              updates(message as PlayerDanmakuAiRecommendedLevelV2))\n          as PlayerDanmakuAiRecommendedLevelV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuAiRecommendedLevelV2 create() =>\n      PlayerDanmakuAiRecommendedLevelV2._();\n  PlayerDanmakuAiRecommendedLevelV2 createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuAiRecommendedLevelV2> createRepeated() =>\n      $pb.PbList<PlayerDanmakuAiRecommendedLevelV2>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuAiRecommendedLevelV2 getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuAiRecommendedLevelV2>(\n          create);\n  static PlayerDanmakuAiRecommendedLevelV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.int get value => $_getIZ(0);\n  @$pb.TagNumber(1)\n  set value($core.int v) {\n    $_setSignedInt32(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuAiRecommendedSwitch extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuAiRecommendedSwitch',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuAiRecommendedSwitch._() : super();\n  factory PlayerDanmakuAiRecommendedSwitch({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuAiRecommendedSwitch.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuAiRecommendedSwitch.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuAiRecommendedSwitch clone() =>\n      PlayerDanmakuAiRecommendedSwitch()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuAiRecommendedSwitch copyWith(\n          void Function(PlayerDanmakuAiRecommendedSwitch) updates) =>\n      super.copyWith(\n              (message) => updates(message as PlayerDanmakuAiRecommendedSwitch))\n          as PlayerDanmakuAiRecommendedSwitch; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuAiRecommendedSwitch create() =>\n      PlayerDanmakuAiRecommendedSwitch._();\n  PlayerDanmakuAiRecommendedSwitch createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuAiRecommendedSwitch> createRepeated() =>\n      $pb.PbList<PlayerDanmakuAiRecommendedSwitch>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuAiRecommendedSwitch getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuAiRecommendedSwitch>(\n          create);\n  static PlayerDanmakuAiRecommendedSwitch? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuBlockbottom extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuBlockbottom',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuBlockbottom._() : super();\n  factory PlayerDanmakuBlockbottom({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuBlockbottom.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuBlockbottom.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockbottom clone() =>\n      PlayerDanmakuBlockbottom()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockbottom copyWith(\n          void Function(PlayerDanmakuBlockbottom) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuBlockbottom))\n          as PlayerDanmakuBlockbottom; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockbottom create() => PlayerDanmakuBlockbottom._();\n  PlayerDanmakuBlockbottom createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuBlockbottom> createRepeated() =>\n      $pb.PbList<PlayerDanmakuBlockbottom>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockbottom getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuBlockbottom>(create);\n  static PlayerDanmakuBlockbottom? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuBlockcolorful extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuBlockcolorful',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuBlockcolorful._() : super();\n  factory PlayerDanmakuBlockcolorful({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuBlockcolorful.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuBlockcolorful.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockcolorful clone() =>\n      PlayerDanmakuBlockcolorful()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockcolorful copyWith(\n          void Function(PlayerDanmakuBlockcolorful) updates) =>\n      super.copyWith(\n              (message) => updates(message as PlayerDanmakuBlockcolorful))\n          as PlayerDanmakuBlockcolorful; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockcolorful create() => PlayerDanmakuBlockcolorful._();\n  PlayerDanmakuBlockcolorful createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuBlockcolorful> createRepeated() =>\n      $pb.PbList<PlayerDanmakuBlockcolorful>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockcolorful getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuBlockcolorful>(create);\n  static PlayerDanmakuBlockcolorful? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuBlockrepeat extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuBlockrepeat',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuBlockrepeat._() : super();\n  factory PlayerDanmakuBlockrepeat({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuBlockrepeat.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuBlockrepeat.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockrepeat clone() =>\n      PlayerDanmakuBlockrepeat()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockrepeat copyWith(\n          void Function(PlayerDanmakuBlockrepeat) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuBlockrepeat))\n          as PlayerDanmakuBlockrepeat; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockrepeat create() => PlayerDanmakuBlockrepeat._();\n  PlayerDanmakuBlockrepeat createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuBlockrepeat> createRepeated() =>\n      $pb.PbList<PlayerDanmakuBlockrepeat>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockrepeat getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuBlockrepeat>(create);\n  static PlayerDanmakuBlockrepeat? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuBlockscroll extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuBlockscroll',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuBlockscroll._() : super();\n  factory PlayerDanmakuBlockscroll({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuBlockscroll.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuBlockscroll.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockscroll clone() =>\n      PlayerDanmakuBlockscroll()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockscroll copyWith(\n          void Function(PlayerDanmakuBlockscroll) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuBlockscroll))\n          as PlayerDanmakuBlockscroll; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockscroll create() => PlayerDanmakuBlockscroll._();\n  PlayerDanmakuBlockscroll createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuBlockscroll> createRepeated() =>\n      $pb.PbList<PlayerDanmakuBlockscroll>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockscroll getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuBlockscroll>(create);\n  static PlayerDanmakuBlockscroll? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuBlockspecial extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuBlockspecial',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuBlockspecial._() : super();\n  factory PlayerDanmakuBlockspecial({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuBlockspecial.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuBlockspecial.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockspecial clone() =>\n      PlayerDanmakuBlockspecial()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlockspecial copyWith(\n          void Function(PlayerDanmakuBlockspecial) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuBlockspecial))\n          as PlayerDanmakuBlockspecial; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockspecial create() => PlayerDanmakuBlockspecial._();\n  PlayerDanmakuBlockspecial createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuBlockspecial> createRepeated() =>\n      $pb.PbList<PlayerDanmakuBlockspecial>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlockspecial getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuBlockspecial>(create);\n  static PlayerDanmakuBlockspecial? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuBlocktop extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuBlocktop',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuBlocktop._() : super();\n  factory PlayerDanmakuBlocktop({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuBlocktop.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuBlocktop.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlocktop clone() =>\n      PlayerDanmakuBlocktop()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuBlocktop copyWith(\n          void Function(PlayerDanmakuBlocktop) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuBlocktop))\n          as PlayerDanmakuBlocktop; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlocktop create() => PlayerDanmakuBlocktop._();\n  PlayerDanmakuBlocktop createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuBlocktop> createRepeated() =>\n      $pb.PbList<PlayerDanmakuBlocktop>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuBlocktop getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuBlocktop>(create);\n  static PlayerDanmakuBlocktop? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuDomain extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuDomain',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.double>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value',\n        $pb.PbFieldType.OF)\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuDomain._() : super();\n  factory PlayerDanmakuDomain({\n    $core.double? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuDomain.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuDomain.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuDomain clone() => PlayerDanmakuDomain()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuDomain copyWith(void Function(PlayerDanmakuDomain) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuDomain))\n          as PlayerDanmakuDomain; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuDomain create() => PlayerDanmakuDomain._();\n  PlayerDanmakuDomain createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuDomain> createRepeated() =>\n      $pb.PbList<PlayerDanmakuDomain>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuDomain getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuDomain>(create);\n  static PlayerDanmakuDomain? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.double get value => $_getN(0);\n  @$pb.TagNumber(1)\n  set value($core.double v) {\n    $_setFloat(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuEnableblocklist extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuEnableblocklist',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuEnableblocklist._() : super();\n  factory PlayerDanmakuEnableblocklist({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuEnableblocklist.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuEnableblocklist.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuEnableblocklist clone() =>\n      PlayerDanmakuEnableblocklist()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuEnableblocklist copyWith(\n          void Function(PlayerDanmakuEnableblocklist) updates) =>\n      super.copyWith(\n              (message) => updates(message as PlayerDanmakuEnableblocklist))\n          as PlayerDanmakuEnableblocklist; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuEnableblocklist create() =>\n      PlayerDanmakuEnableblocklist._();\n  PlayerDanmakuEnableblocklist createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuEnableblocklist> createRepeated() =>\n      $pb.PbList<PlayerDanmakuEnableblocklist>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuEnableblocklist getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuEnableblocklist>(create);\n  static PlayerDanmakuEnableblocklist? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuOpacity extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuOpacity',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.double>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value',\n        $pb.PbFieldType.OF)\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuOpacity._() : super();\n  factory PlayerDanmakuOpacity({\n    $core.double? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuOpacity.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuOpacity.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuOpacity clone() =>\n      PlayerDanmakuOpacity()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuOpacity copyWith(void Function(PlayerDanmakuOpacity) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuOpacity))\n          as PlayerDanmakuOpacity; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuOpacity create() => PlayerDanmakuOpacity._();\n  PlayerDanmakuOpacity createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuOpacity> createRepeated() =>\n      $pb.PbList<PlayerDanmakuOpacity>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuOpacity getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuOpacity>(create);\n  static PlayerDanmakuOpacity? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.double get value => $_getN(0);\n  @$pb.TagNumber(1)\n  set value($core.double v) {\n    $_setFloat(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuScalingfactor extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuScalingfactor',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.double>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value',\n        $pb.PbFieldType.OF)\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuScalingfactor._() : super();\n  factory PlayerDanmakuScalingfactor({\n    $core.double? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuScalingfactor.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuScalingfactor.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuScalingfactor clone() =>\n      PlayerDanmakuScalingfactor()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuScalingfactor copyWith(\n          void Function(PlayerDanmakuScalingfactor) updates) =>\n      super.copyWith(\n              (message) => updates(message as PlayerDanmakuScalingfactor))\n          as PlayerDanmakuScalingfactor; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuScalingfactor create() => PlayerDanmakuScalingfactor._();\n  PlayerDanmakuScalingfactor createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuScalingfactor> createRepeated() =>\n      $pb.PbList<PlayerDanmakuScalingfactor>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuScalingfactor getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuScalingfactor>(create);\n  static PlayerDanmakuScalingfactor? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.double get value => $_getN(0);\n  @$pb.TagNumber(1)\n  set value($core.double v) {\n    $_setFloat(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuSeniorModeSwitch extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuSeniorModeSwitch',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.int>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuSeniorModeSwitch._() : super();\n  factory PlayerDanmakuSeniorModeSwitch({\n    $core.int? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuSeniorModeSwitch.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuSeniorModeSwitch.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuSeniorModeSwitch clone() =>\n      PlayerDanmakuSeniorModeSwitch()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuSeniorModeSwitch copyWith(\n          void Function(PlayerDanmakuSeniorModeSwitch) updates) =>\n      super.copyWith(\n              (message) => updates(message as PlayerDanmakuSeniorModeSwitch))\n          as PlayerDanmakuSeniorModeSwitch; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuSeniorModeSwitch create() =>\n      PlayerDanmakuSeniorModeSwitch._();\n  PlayerDanmakuSeniorModeSwitch createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuSeniorModeSwitch> createRepeated() =>\n      $pb.PbList<PlayerDanmakuSeniorModeSwitch>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuSeniorModeSwitch getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuSeniorModeSwitch>(create);\n  static PlayerDanmakuSeniorModeSwitch? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.int get value => $_getIZ(0);\n  @$pb.TagNumber(1)\n  set value($core.int v) {\n    $_setSignedInt32(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuSpeed extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuSpeed',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.int>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuSpeed._() : super();\n  factory PlayerDanmakuSpeed({\n    $core.int? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuSpeed.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuSpeed.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuSpeed clone() => PlayerDanmakuSpeed()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuSpeed copyWith(void Function(PlayerDanmakuSpeed) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuSpeed))\n          as PlayerDanmakuSpeed; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuSpeed create() => PlayerDanmakuSpeed._();\n  PlayerDanmakuSpeed createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuSpeed> createRepeated() =>\n      $pb.PbList<PlayerDanmakuSpeed>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuSpeed getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuSpeed>(create);\n  static PlayerDanmakuSpeed? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.int get value => $_getIZ(0);\n  @$pb.TagNumber(1)\n  set value($core.int v) {\n    $_setSignedInt32(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuSwitch extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuSwitch',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..aOB(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'canIgnore')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuSwitch._() : super();\n  factory PlayerDanmakuSwitch({\n    $core.bool? value,\n    $core.bool? canIgnore,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    if (canIgnore != null) {\n      _result.canIgnore = canIgnore;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuSwitch.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuSwitch.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuSwitch clone() => PlayerDanmakuSwitch()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuSwitch copyWith(void Function(PlayerDanmakuSwitch) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuSwitch))\n          as PlayerDanmakuSwitch; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuSwitch create() => PlayerDanmakuSwitch._();\n  PlayerDanmakuSwitch createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuSwitch> createRepeated() =>\n      $pb.PbList<PlayerDanmakuSwitch>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuSwitch getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuSwitch>(create);\n  static PlayerDanmakuSwitch? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.bool get canIgnore => $_getBF(1);\n  @$pb.TagNumber(2)\n  set canIgnore($core.bool v) {\n    $_setBool(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasCanIgnore() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearCanIgnore() => clearField(2);\n}\n\nclass PlayerDanmakuSwitchSave extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuSwitchSave',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuSwitchSave._() : super();\n  factory PlayerDanmakuSwitchSave({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuSwitchSave.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuSwitchSave.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuSwitchSave clone() =>\n      PlayerDanmakuSwitchSave()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuSwitchSave copyWith(\n          void Function(PlayerDanmakuSwitchSave) updates) =>\n      super.copyWith((message) => updates(message as PlayerDanmakuSwitchSave))\n          as PlayerDanmakuSwitchSave; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuSwitchSave create() => PlayerDanmakuSwitchSave._();\n  PlayerDanmakuSwitchSave createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuSwitchSave> createRepeated() =>\n      $pb.PbList<PlayerDanmakuSwitchSave>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuSwitchSave getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuSwitchSave>(create);\n  static PlayerDanmakuSwitchSave? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PlayerDanmakuUseDefaultConfig extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PlayerDanmakuUseDefaultConfig',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOB(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'value')\n    ..hasRequiredFields = false;\n\n  PlayerDanmakuUseDefaultConfig._() : super();\n  factory PlayerDanmakuUseDefaultConfig({\n    $core.bool? value,\n  }) {\n    final _result = create();\n    if (value != null) {\n      _result.value = value;\n    }\n    return _result;\n  }\n  factory PlayerDanmakuUseDefaultConfig.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PlayerDanmakuUseDefaultConfig.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuUseDefaultConfig clone() =>\n      PlayerDanmakuUseDefaultConfig()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PlayerDanmakuUseDefaultConfig copyWith(\n          void Function(PlayerDanmakuUseDefaultConfig) updates) =>\n      super.copyWith(\n              (message) => updates(message as PlayerDanmakuUseDefaultConfig))\n          as PlayerDanmakuUseDefaultConfig; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuUseDefaultConfig create() =>\n      PlayerDanmakuUseDefaultConfig._();\n  PlayerDanmakuUseDefaultConfig createEmptyInstance() => create();\n  static $pb.PbList<PlayerDanmakuUseDefaultConfig> createRepeated() =>\n      $pb.PbList<PlayerDanmakuUseDefaultConfig>();\n  @$core.pragma('dart2js:noInline')\n  static PlayerDanmakuUseDefaultConfig getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PlayerDanmakuUseDefaultConfig>(create);\n  static PlayerDanmakuUseDefaultConfig? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.bool get value => $_getBF(0);\n  @$pb.TagNumber(1)\n  set value($core.bool v) {\n    $_setBool(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasValue() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearValue() => clearField(1);\n}\n\nclass PostPanel extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PostPanel',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'start')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'end')\n    ..aInt64(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'priority')\n    ..aInt64(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'bizId')\n    ..e<PostPanelBizType>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'bizType',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: PostPanelBizType.PostPanelBizTypeNone,\n        valueOf: PostPanelBizType.valueOf,\n        enumValues: PostPanelBizType.values)\n    ..aOM<ClickButton>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'clickButton',\n        subBuilder: ClickButton.create)\n    ..aOM<TextInput>(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'textInput',\n        subBuilder: TextInput.create)\n    ..aOM<CheckBox>(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'checkBox',\n        subBuilder: CheckBox.create)\n    ..aOM<Toast>(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'toast',\n        subBuilder: Toast.create)\n    ..hasRequiredFields = false;\n\n  PostPanel._() : super();\n  factory PostPanel({\n    $fixnum.Int64? start,\n    $fixnum.Int64? end,\n    $fixnum.Int64? priority,\n    $fixnum.Int64? bizId,\n    PostPanelBizType? bizType,\n    ClickButton? clickButton,\n    TextInput? textInput,\n    CheckBox? checkBox,\n    Toast? toast,\n  }) {\n    final _result = create();\n    if (start != null) {\n      _result.start = start;\n    }\n    if (end != null) {\n      _result.end = end;\n    }\n    if (priority != null) {\n      _result.priority = priority;\n    }\n    if (bizId != null) {\n      _result.bizId = bizId;\n    }\n    if (bizType != null) {\n      _result.bizType = bizType;\n    }\n    if (clickButton != null) {\n      _result.clickButton = clickButton;\n    }\n    if (textInput != null) {\n      _result.textInput = textInput;\n    }\n    if (checkBox != null) {\n      _result.checkBox = checkBox;\n    }\n    if (toast != null) {\n      _result.toast = toast;\n    }\n    return _result;\n  }\n  factory PostPanel.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PostPanel.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PostPanel clone() => PostPanel()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PostPanel copyWith(void Function(PostPanel) updates) =>\n      super.copyWith((message) => updates(message as PostPanel))\n          as PostPanel; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PostPanel create() => PostPanel._();\n  PostPanel createEmptyInstance() => create();\n  static $pb.PbList<PostPanel> createRepeated() => $pb.PbList<PostPanel>();\n  @$core.pragma('dart2js:noInline')\n  static PostPanel getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PostPanel>(create);\n  static PostPanel? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get start => $_getI64(0);\n  @$pb.TagNumber(1)\n  set start($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasStart() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearStart() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get end => $_getI64(1);\n  @$pb.TagNumber(2)\n  set end($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasEnd() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearEnd() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $fixnum.Int64 get priority => $_getI64(2);\n  @$pb.TagNumber(3)\n  set priority($fixnum.Int64 v) {\n    $_setInt64(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasPriority() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearPriority() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $fixnum.Int64 get bizId => $_getI64(3);\n  @$pb.TagNumber(4)\n  set bizId($fixnum.Int64 v) {\n    $_setInt64(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasBizId() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearBizId() => clearField(4);\n\n  @$pb.TagNumber(5)\n  PostPanelBizType get bizType => $_getN(4);\n  @$pb.TagNumber(5)\n  set bizType(PostPanelBizType v) {\n    setField(5, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasBizType() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearBizType() => clearField(5);\n\n  @$pb.TagNumber(6)\n  ClickButton get clickButton => $_getN(5);\n  @$pb.TagNumber(6)\n  set clickButton(ClickButton v) {\n    setField(6, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasClickButton() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearClickButton() => clearField(6);\n  @$pb.TagNumber(6)\n  ClickButton ensureClickButton() => $_ensure(5);\n\n  @$pb.TagNumber(7)\n  TextInput get textInput => $_getN(6);\n  @$pb.TagNumber(7)\n  set textInput(TextInput v) {\n    setField(7, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasTextInput() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearTextInput() => clearField(7);\n  @$pb.TagNumber(7)\n  TextInput ensureTextInput() => $_ensure(6);\n\n  @$pb.TagNumber(8)\n  CheckBox get checkBox => $_getN(7);\n  @$pb.TagNumber(8)\n  set checkBox(CheckBox v) {\n    setField(8, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasCheckBox() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearCheckBox() => clearField(8);\n  @$pb.TagNumber(8)\n  CheckBox ensureCheckBox() => $_ensure(7);\n\n  @$pb.TagNumber(9)\n  Toast get toast => $_getN(8);\n  @$pb.TagNumber(9)\n  set toast(Toast v) {\n    setField(9, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasToast() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearToast() => clearField(9);\n  @$pb.TagNumber(9)\n  Toast ensureToast() => $_ensure(8);\n}\n\nclass PostPanelV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'PostPanelV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'start')\n    ..aInt64(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'end')\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'bizType',\n        $pb.PbFieldType.O3)\n    ..aOM<ClickButtonV2>(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'clickButton',\n        subBuilder: ClickButtonV2.create)\n    ..aOM<TextInputV2>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'textInput',\n        subBuilder: TextInputV2.create)\n    ..aOM<CheckBoxV2>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'checkBox',\n        subBuilder: CheckBoxV2.create)\n    ..aOM<ToastV2>(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'toast',\n        subBuilder: ToastV2.create)\n    ..aOM<BubbleV2>(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'bubble',\n        subBuilder: BubbleV2.create)\n    ..aOM<LabelV2>(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'label',\n        subBuilder: LabelV2.create)\n    ..a<$core.int>(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'postStatus',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  PostPanelV2._() : super();\n  factory PostPanelV2({\n    $fixnum.Int64? start,\n    $fixnum.Int64? end,\n    $core.int? bizType,\n    ClickButtonV2? clickButton,\n    TextInputV2? textInput,\n    CheckBoxV2? checkBox,\n    ToastV2? toast,\n    BubbleV2? bubble,\n    LabelV2? label,\n    $core.int? postStatus,\n  }) {\n    final _result = create();\n    if (start != null) {\n      _result.start = start;\n    }\n    if (end != null) {\n      _result.end = end;\n    }\n    if (bizType != null) {\n      _result.bizType = bizType;\n    }\n    if (clickButton != null) {\n      _result.clickButton = clickButton;\n    }\n    if (textInput != null) {\n      _result.textInput = textInput;\n    }\n    if (checkBox != null) {\n      _result.checkBox = checkBox;\n    }\n    if (toast != null) {\n      _result.toast = toast;\n    }\n    if (bubble != null) {\n      _result.bubble = bubble;\n    }\n    if (label != null) {\n      _result.label = label;\n    }\n    if (postStatus != null) {\n      _result.postStatus = postStatus;\n    }\n    return _result;\n  }\n  factory PostPanelV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory PostPanelV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  PostPanelV2 clone() => PostPanelV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  PostPanelV2 copyWith(void Function(PostPanelV2) updates) =>\n      super.copyWith((message) => updates(message as PostPanelV2))\n          as PostPanelV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static PostPanelV2 create() => PostPanelV2._();\n  PostPanelV2 createEmptyInstance() => create();\n  static $pb.PbList<PostPanelV2> createRepeated() => $pb.PbList<PostPanelV2>();\n  @$core.pragma('dart2js:noInline')\n  static PostPanelV2 getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<PostPanelV2>(create);\n  static PostPanelV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get start => $_getI64(0);\n  @$pb.TagNumber(1)\n  set start($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasStart() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearStart() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $fixnum.Int64 get end => $_getI64(1);\n  @$pb.TagNumber(2)\n  set end($fixnum.Int64 v) {\n    $_setInt64(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasEnd() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearEnd() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get bizType => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set bizType($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasBizType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearBizType() => clearField(3);\n\n  @$pb.TagNumber(4)\n  ClickButtonV2 get clickButton => $_getN(3);\n  @$pb.TagNumber(4)\n  set clickButton(ClickButtonV2 v) {\n    setField(4, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasClickButton() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearClickButton() => clearField(4);\n  @$pb.TagNumber(4)\n  ClickButtonV2 ensureClickButton() => $_ensure(3);\n\n  @$pb.TagNumber(5)\n  TextInputV2 get textInput => $_getN(4);\n  @$pb.TagNumber(5)\n  set textInput(TextInputV2 v) {\n    setField(5, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasTextInput() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearTextInput() => clearField(5);\n  @$pb.TagNumber(5)\n  TextInputV2 ensureTextInput() => $_ensure(4);\n\n  @$pb.TagNumber(6)\n  CheckBoxV2 get checkBox => $_getN(5);\n  @$pb.TagNumber(6)\n  set checkBox(CheckBoxV2 v) {\n    setField(6, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasCheckBox() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearCheckBox() => clearField(6);\n  @$pb.TagNumber(6)\n  CheckBoxV2 ensureCheckBox() => $_ensure(5);\n\n  @$pb.TagNumber(7)\n  ToastV2 get toast => $_getN(6);\n  @$pb.TagNumber(7)\n  set toast(ToastV2 v) {\n    setField(7, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasToast() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearToast() => clearField(7);\n  @$pb.TagNumber(7)\n  ToastV2 ensureToast() => $_ensure(6);\n\n  @$pb.TagNumber(8)\n  BubbleV2 get bubble => $_getN(7);\n  @$pb.TagNumber(8)\n  set bubble(BubbleV2 v) {\n    setField(8, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasBubble() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearBubble() => clearField(8);\n  @$pb.TagNumber(8)\n  BubbleV2 ensureBubble() => $_ensure(7);\n\n  @$pb.TagNumber(9)\n  LabelV2 get label => $_getN(8);\n  @$pb.TagNumber(9)\n  set label(LabelV2 v) {\n    setField(9, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasLabel() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearLabel() => clearField(9);\n  @$pb.TagNumber(9)\n  LabelV2 ensureLabel() => $_ensure(8);\n\n  @$pb.TagNumber(10)\n  $core.int get postStatus => $_getIZ(9);\n  @$pb.TagNumber(10)\n  set postStatus($core.int v) {\n    $_setSignedInt32(9, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasPostStatus() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearPostStatus() => clearField(10);\n}\n\nclass Response extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Response',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..a<$core.int>(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'code',\n        $pb.PbFieldType.O3)\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'message')\n    ..hasRequiredFields = false;\n\n  Response._() : super();\n  factory Response({\n    $core.int? code,\n    $core.String? message,\n  }) {\n    final _result = create();\n    if (code != null) {\n      _result.code = code;\n    }\n    if (message != null) {\n      _result.message = message;\n    }\n    return _result;\n  }\n  factory Response.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Response.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Response clone() => Response()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Response copyWith(void Function(Response) updates) =>\n      super.copyWith((message) => updates(message as Response))\n          as Response; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Response create() => Response._();\n  Response createEmptyInstance() => create();\n  static $pb.PbList<Response> createRepeated() => $pb.PbList<Response>();\n  @$core.pragma('dart2js:noInline')\n  static Response getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Response>(create);\n  static Response? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.int get code => $_getIZ(0);\n  @$pb.TagNumber(1)\n  set code($core.int v) {\n    $_setSignedInt32(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasCode() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearCode() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get message => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set message($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasMessage() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearMessage() => clearField(2);\n}\n\nclass SubtitleItem extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'SubtitleItem',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'id')\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'idStr')\n    ..aOS(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'lan')\n    ..aOS(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'lanDoc')\n    ..aOS(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'subtitleUrl')\n    ..aOM<UserInfo>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'author',\n        subBuilder: UserInfo.create)\n    ..e<SubtitleType>(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'type',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: SubtitleType.CC,\n        valueOf: SubtitleType.valueOf,\n        enumValues: SubtitleType.values)\n    ..aOS(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'lanDocBrief')\n    ..e<SubtitleAiType>(\n        9,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiType',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: SubtitleAiType.Normal,\n        valueOf: SubtitleAiType.valueOf,\n        enumValues: SubtitleAiType.values)\n    ..e<SubtitleAiStatus>(\n        10,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'aiStatus',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: SubtitleAiStatus.None,\n        valueOf: SubtitleAiStatus.valueOf,\n        enumValues: SubtitleAiStatus.values)\n    ..hasRequiredFields = false;\n\n  SubtitleItem._() : super();\n  factory SubtitleItem({\n    $fixnum.Int64? id,\n    $core.String? idStr,\n    $core.String? lan,\n    $core.String? lanDoc,\n    $core.String? subtitleUrl,\n    UserInfo? author,\n    SubtitleType? type,\n    $core.String? lanDocBrief,\n    SubtitleAiType? aiType,\n    SubtitleAiStatus? aiStatus,\n  }) {\n    final _result = create();\n    if (id != null) {\n      _result.id = id;\n    }\n    if (idStr != null) {\n      _result.idStr = idStr;\n    }\n    if (lan != null) {\n      _result.lan = lan;\n    }\n    if (lanDoc != null) {\n      _result.lanDoc = lanDoc;\n    }\n    if (subtitleUrl != null) {\n      _result.subtitleUrl = subtitleUrl;\n    }\n    if (author != null) {\n      _result.author = author;\n    }\n    if (type != null) {\n      _result.type = type;\n    }\n    if (lanDocBrief != null) {\n      _result.lanDocBrief = lanDocBrief;\n    }\n    if (aiType != null) {\n      _result.aiType = aiType;\n    }\n    if (aiStatus != null) {\n      _result.aiStatus = aiStatus;\n    }\n    return _result;\n  }\n  factory SubtitleItem.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory SubtitleItem.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  SubtitleItem clone() => SubtitleItem()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  SubtitleItem copyWith(void Function(SubtitleItem) updates) =>\n      super.copyWith((message) => updates(message as SubtitleItem))\n          as SubtitleItem; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static SubtitleItem create() => SubtitleItem._();\n  SubtitleItem createEmptyInstance() => create();\n  static $pb.PbList<SubtitleItem> createRepeated() =>\n      $pb.PbList<SubtitleItem>();\n  @$core.pragma('dart2js:noInline')\n  static SubtitleItem getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<SubtitleItem>(create);\n  static SubtitleItem? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get id => $_getI64(0);\n  @$pb.TagNumber(1)\n  set id($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasId() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearId() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get idStr => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set idStr($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasIdStr() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearIdStr() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.String get lan => $_getSZ(2);\n  @$pb.TagNumber(3)\n  set lan($core.String v) {\n    $_setString(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasLan() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearLan() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.String get lanDoc => $_getSZ(3);\n  @$pb.TagNumber(4)\n  set lanDoc($core.String v) {\n    $_setString(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasLanDoc() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearLanDoc() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.String get subtitleUrl => $_getSZ(4);\n  @$pb.TagNumber(5)\n  set subtitleUrl($core.String v) {\n    $_setString(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasSubtitleUrl() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearSubtitleUrl() => clearField(5);\n\n  @$pb.TagNumber(6)\n  UserInfo get author => $_getN(5);\n  @$pb.TagNumber(6)\n  set author(UserInfo v) {\n    setField(6, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasAuthor() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearAuthor() => clearField(6);\n  @$pb.TagNumber(6)\n  UserInfo ensureAuthor() => $_ensure(5);\n\n  @$pb.TagNumber(7)\n  SubtitleType get type => $_getN(6);\n  @$pb.TagNumber(7)\n  set type(SubtitleType v) {\n    setField(7, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasType() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearType() => clearField(7);\n\n  @$pb.TagNumber(8)\n  $core.String get lanDocBrief => $_getSZ(7);\n  @$pb.TagNumber(8)\n  set lanDocBrief($core.String v) {\n    $_setString(7, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasLanDocBrief() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearLanDocBrief() => clearField(8);\n\n  @$pb.TagNumber(9)\n  SubtitleAiType get aiType => $_getN(8);\n  @$pb.TagNumber(9)\n  set aiType(SubtitleAiType v) {\n    setField(9, v);\n  }\n\n  @$pb.TagNumber(9)\n  $core.bool hasAiType() => $_has(8);\n  @$pb.TagNumber(9)\n  void clearAiType() => clearField(9);\n\n  @$pb.TagNumber(10)\n  SubtitleAiStatus get aiStatus => $_getN(9);\n  @$pb.TagNumber(10)\n  set aiStatus(SubtitleAiStatus v) {\n    setField(10, v);\n  }\n\n  @$pb.TagNumber(10)\n  $core.bool hasAiStatus() => $_has(9);\n  @$pb.TagNumber(10)\n  void clearAiStatus() => clearField(10);\n}\n\nclass TextInput extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'TextInput',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pPS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'portraitPlaceholder')\n    ..pPS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'landscapePlaceholder')\n    ..e<RenderType>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'renderType',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: RenderType.RenderTypeNone,\n        valueOf: RenderType.valueOf,\n        enumValues: RenderType.values)\n    ..aOB(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'placeholderPost')\n    ..aOB(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'show')\n    ..pc<Avatar>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'avatar',\n        $pb.PbFieldType.PM,\n        subBuilder: Avatar.create)\n    ..e<PostStatus>(\n        7,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'postStatus',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: PostStatus.PostStatusNormal,\n        valueOf: PostStatus.valueOf,\n        enumValues: PostStatus.values)\n    ..aOM<Label>(\n        8,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'label',\n        subBuilder: Label.create)\n    ..hasRequiredFields = false;\n\n  TextInput._() : super();\n  factory TextInput({\n    $core.Iterable<$core.String>? portraitPlaceholder,\n    $core.Iterable<$core.String>? landscapePlaceholder,\n    RenderType? renderType,\n    $core.bool? placeholderPost,\n    $core.bool? show,\n    $core.Iterable<Avatar>? avatar,\n    PostStatus? postStatus,\n    Label? label,\n  }) {\n    final _result = create();\n    if (portraitPlaceholder != null) {\n      _result.portraitPlaceholder.addAll(portraitPlaceholder);\n    }\n    if (landscapePlaceholder != null) {\n      _result.landscapePlaceholder.addAll(landscapePlaceholder);\n    }\n    if (renderType != null) {\n      _result.renderType = renderType;\n    }\n    if (placeholderPost != null) {\n      _result.placeholderPost = placeholderPost;\n    }\n    if (show != null) {\n      _result.show = show;\n    }\n    if (avatar != null) {\n      _result.avatar.addAll(avatar);\n    }\n    if (postStatus != null) {\n      _result.postStatus = postStatus;\n    }\n    if (label != null) {\n      _result.label = label;\n    }\n    return _result;\n  }\n  factory TextInput.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory TextInput.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  TextInput clone() => TextInput()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  TextInput copyWith(void Function(TextInput) updates) =>\n      super.copyWith((message) => updates(message as TextInput))\n          as TextInput; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static TextInput create() => TextInput._();\n  TextInput createEmptyInstance() => create();\n  static $pb.PbList<TextInput> createRepeated() => $pb.PbList<TextInput>();\n  @$core.pragma('dart2js:noInline')\n  static TextInput getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<TextInput>(create);\n  static TextInput? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<$core.String> get portraitPlaceholder => $_getList(0);\n\n  @$pb.TagNumber(2)\n  $core.List<$core.String> get landscapePlaceholder => $_getList(1);\n\n  @$pb.TagNumber(3)\n  RenderType get renderType => $_getN(2);\n  @$pb.TagNumber(3)\n  set renderType(RenderType v) {\n    setField(3, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasRenderType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearRenderType() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.bool get placeholderPost => $_getBF(3);\n  @$pb.TagNumber(4)\n  set placeholderPost($core.bool v) {\n    $_setBool(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasPlaceholderPost() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearPlaceholderPost() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.bool get show => $_getBF(4);\n  @$pb.TagNumber(5)\n  set show($core.bool v) {\n    $_setBool(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasShow() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearShow() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.List<Avatar> get avatar => $_getList(5);\n\n  @$pb.TagNumber(7)\n  PostStatus get postStatus => $_getN(6);\n  @$pb.TagNumber(7)\n  set postStatus(PostStatus v) {\n    setField(7, v);\n  }\n\n  @$pb.TagNumber(7)\n  $core.bool hasPostStatus() => $_has(6);\n  @$pb.TagNumber(7)\n  void clearPostStatus() => clearField(7);\n\n  @$pb.TagNumber(8)\n  Label get label => $_getN(7);\n  @$pb.TagNumber(8)\n  set label(Label v) {\n    setField(8, v);\n  }\n\n  @$pb.TagNumber(8)\n  $core.bool hasLabel() => $_has(7);\n  @$pb.TagNumber(8)\n  void clearLabel() => clearField(8);\n  @$pb.TagNumber(8)\n  Label ensureLabel() => $_ensure(7);\n}\n\nclass TextInputV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'TextInputV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..pPS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'portraitPlaceholder')\n    ..pPS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'landscapePlaceholder')\n    ..e<RenderType>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'renderType',\n        $pb.PbFieldType.OE,\n        defaultOrMaker: RenderType.RenderTypeNone,\n        valueOf: RenderType.valueOf,\n        enumValues: RenderType.values)\n    ..aOB(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'placeholderPost')\n    ..pc<Avatar>(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'avatar',\n        $pb.PbFieldType.PM,\n        subBuilder: Avatar.create)\n    ..a<$core.int>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'textInputLimit',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  TextInputV2._() : super();\n  factory TextInputV2({\n    $core.Iterable<$core.String>? portraitPlaceholder,\n    $core.Iterable<$core.String>? landscapePlaceholder,\n    RenderType? renderType,\n    $core.bool? placeholderPost,\n    $core.Iterable<Avatar>? avatar,\n    $core.int? textInputLimit,\n  }) {\n    final _result = create();\n    if (portraitPlaceholder != null) {\n      _result.portraitPlaceholder.addAll(portraitPlaceholder);\n    }\n    if (landscapePlaceholder != null) {\n      _result.landscapePlaceholder.addAll(landscapePlaceholder);\n    }\n    if (renderType != null) {\n      _result.renderType = renderType;\n    }\n    if (placeholderPost != null) {\n      _result.placeholderPost = placeholderPost;\n    }\n    if (avatar != null) {\n      _result.avatar.addAll(avatar);\n    }\n    if (textInputLimit != null) {\n      _result.textInputLimit = textInputLimit;\n    }\n    return _result;\n  }\n  factory TextInputV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory TextInputV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  TextInputV2 clone() => TextInputV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  TextInputV2 copyWith(void Function(TextInputV2) updates) =>\n      super.copyWith((message) => updates(message as TextInputV2))\n          as TextInputV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static TextInputV2 create() => TextInputV2._();\n  TextInputV2 createEmptyInstance() => create();\n  static $pb.PbList<TextInputV2> createRepeated() => $pb.PbList<TextInputV2>();\n  @$core.pragma('dart2js:noInline')\n  static TextInputV2 getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<TextInputV2>(create);\n  static TextInputV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.List<$core.String> get portraitPlaceholder => $_getList(0);\n\n  @$pb.TagNumber(2)\n  $core.List<$core.String> get landscapePlaceholder => $_getList(1);\n\n  @$pb.TagNumber(3)\n  RenderType get renderType => $_getN(2);\n  @$pb.TagNumber(3)\n  set renderType(RenderType v) {\n    setField(3, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasRenderType() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearRenderType() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.bool get placeholderPost => $_getBF(3);\n  @$pb.TagNumber(4)\n  set placeholderPost($core.bool v) {\n    $_setBool(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasPlaceholderPost() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearPlaceholderPost() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.List<Avatar> get avatar => $_getList(4);\n\n  @$pb.TagNumber(6)\n  $core.int get textInputLimit => $_getIZ(5);\n  @$pb.TagNumber(6)\n  set textInputLimit($core.int v) {\n    $_setSignedInt32(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasTextInputLimit() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearTextInputLimit() => clearField(6);\n}\n\nclass Toast extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'Toast',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'duration',\n        $pb.PbFieldType.O3)\n    ..aOB(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'show')\n    ..aOM<Button>(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'button',\n        subBuilder: Button.create)\n    ..hasRequiredFields = false;\n\n  Toast._() : super();\n  factory Toast({\n    $core.String? text,\n    $core.int? duration,\n    $core.bool? show,\n    Button? button,\n  }) {\n    final _result = create();\n    if (text != null) {\n      _result.text = text;\n    }\n    if (duration != null) {\n      _result.duration = duration;\n    }\n    if (show != null) {\n      _result.show = show;\n    }\n    if (button != null) {\n      _result.button = button;\n    }\n    return _result;\n  }\n  factory Toast.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory Toast.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  Toast clone() => Toast()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  Toast copyWith(void Function(Toast) updates) =>\n      super.copyWith((message) => updates(message as Toast))\n          as Toast; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static Toast create() => Toast._();\n  Toast createEmptyInstance() => create();\n  static $pb.PbList<Toast> createRepeated() => $pb.PbList<Toast>();\n  @$core.pragma('dart2js:noInline')\n  static Toast getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Toast>(create);\n  static Toast? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get text => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set text($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearText() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.int get duration => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set duration($core.int v) {\n    $_setSignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasDuration() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearDuration() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.bool get show => $_getBF(2);\n  @$pb.TagNumber(3)\n  set show($core.bool v) {\n    $_setBool(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasShow() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearShow() => clearField(3);\n\n  @$pb.TagNumber(4)\n  Button get button => $_getN(3);\n  @$pb.TagNumber(4)\n  set button(Button v) {\n    setField(4, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasButton() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearButton() => clearField(4);\n  @$pb.TagNumber(4)\n  Button ensureButton() => $_ensure(3);\n}\n\nclass ToastButtonV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'ToastButtonV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'action',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  ToastButtonV2._() : super();\n  factory ToastButtonV2({\n    $core.String? text,\n    $core.int? action,\n  }) {\n    final _result = create();\n    if (text != null) {\n      _result.text = text;\n    }\n    if (action != null) {\n      _result.action = action;\n    }\n    return _result;\n  }\n  factory ToastButtonV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory ToastButtonV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  ToastButtonV2 clone() => ToastButtonV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  ToastButtonV2 copyWith(void Function(ToastButtonV2) updates) =>\n      super.copyWith((message) => updates(message as ToastButtonV2))\n          as ToastButtonV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static ToastButtonV2 create() => ToastButtonV2._();\n  ToastButtonV2 createEmptyInstance() => create();\n  static $pb.PbList<ToastButtonV2> createRepeated() =>\n      $pb.PbList<ToastButtonV2>();\n  @$core.pragma('dart2js:noInline')\n  static ToastButtonV2 getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<ToastButtonV2>(create);\n  static ToastButtonV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get text => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set text($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearText() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.int get action => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set action($core.int v) {\n    $_setSignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasAction() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearAction() => clearField(2);\n}\n\nclass ToastV2 extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'ToastV2',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'text')\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'duration',\n        $pb.PbFieldType.O3)\n    ..aOM<ToastButtonV2>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'toastButtonV2',\n        subBuilder: ToastButtonV2.create)\n    ..hasRequiredFields = false;\n\n  ToastV2._() : super();\n  factory ToastV2({\n    $core.String? text,\n    $core.int? duration,\n    ToastButtonV2? toastButtonV2,\n  }) {\n    final _result = create();\n    if (text != null) {\n      _result.text = text;\n    }\n    if (duration != null) {\n      _result.duration = duration;\n    }\n    if (toastButtonV2 != null) {\n      _result.toastButtonV2 = toastButtonV2;\n    }\n    return _result;\n  }\n  factory ToastV2.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory ToastV2.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  ToastV2 clone() => ToastV2()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  ToastV2 copyWith(void Function(ToastV2) updates) =>\n      super.copyWith((message) => updates(message as ToastV2))\n          as ToastV2; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static ToastV2 create() => ToastV2._();\n  ToastV2 createEmptyInstance() => create();\n  static $pb.PbList<ToastV2> createRepeated() => $pb.PbList<ToastV2>();\n  @$core.pragma('dart2js:noInline')\n  static ToastV2 getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ToastV2>(create);\n  static ToastV2? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get text => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set text($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasText() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearText() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.int get duration => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set duration($core.int v) {\n    $_setSignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasDuration() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearDuration() => clearField(2);\n\n  @$pb.TagNumber(3)\n  ToastButtonV2 get toastButtonV2 => $_getN(2);\n  @$pb.TagNumber(3)\n  set toastButtonV2(ToastButtonV2 v) {\n    setField(3, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasToastButtonV2() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearToastButtonV2() => clearField(3);\n  @$pb.TagNumber(3)\n  ToastButtonV2 ensureToastButtonV2() => $_ensure(2);\n}\n\nclass UserInfo extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'UserInfo',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'mid')\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'name')\n    ..aOS(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'sex')\n    ..aOS(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'face')\n    ..aOS(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'sign')\n    ..a<$core.int>(\n        6,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'rank',\n        $pb.PbFieldType.O3)\n    ..hasRequiredFields = false;\n\n  UserInfo._() : super();\n  factory UserInfo({\n    $fixnum.Int64? mid,\n    $core.String? name,\n    $core.String? sex,\n    $core.String? face,\n    $core.String? sign,\n    $core.int? rank,\n  }) {\n    final _result = create();\n    if (mid != null) {\n      _result.mid = mid;\n    }\n    if (name != null) {\n      _result.name = name;\n    }\n    if (sex != null) {\n      _result.sex = sex;\n    }\n    if (face != null) {\n      _result.face = face;\n    }\n    if (sign != null) {\n      _result.sign = sign;\n    }\n    if (rank != null) {\n      _result.rank = rank;\n    }\n    return _result;\n  }\n  factory UserInfo.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory UserInfo.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  UserInfo clone() => UserInfo()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  UserInfo copyWith(void Function(UserInfo) updates) =>\n      super.copyWith((message) => updates(message as UserInfo))\n          as UserInfo; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static UserInfo create() => UserInfo._();\n  UserInfo createEmptyInstance() => create();\n  static $pb.PbList<UserInfo> createRepeated() => $pb.PbList<UserInfo>();\n  @$core.pragma('dart2js:noInline')\n  static UserInfo getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<UserInfo>(create);\n  static UserInfo? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get mid => $_getI64(0);\n  @$pb.TagNumber(1)\n  set mid($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasMid() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearMid() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get name => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set name($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasName() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearName() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.String get sex => $_getSZ(2);\n  @$pb.TagNumber(3)\n  set sex($core.String v) {\n    $_setString(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasSex() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearSex() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $core.String get face => $_getSZ(3);\n  @$pb.TagNumber(4)\n  set face($core.String v) {\n    $_setString(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasFace() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearFace() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.String get sign => $_getSZ(4);\n  @$pb.TagNumber(5)\n  set sign($core.String v) {\n    $_setString(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasSign() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearSign() => clearField(5);\n\n  @$pb.TagNumber(6)\n  $core.int get rank => $_getIZ(5);\n  @$pb.TagNumber(6)\n  set rank($core.int v) {\n    $_setSignedInt32(5, v);\n  }\n\n  @$pb.TagNumber(6)\n  $core.bool hasRank() => $_has(5);\n  @$pb.TagNumber(6)\n  void clearRank() => clearField(6);\n}\n\nclass VideoMask extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'VideoMask',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aInt64(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'cid')\n    ..a<$core.int>(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'plat',\n        $pb.PbFieldType.O3)\n    ..a<$core.int>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'fps',\n        $pb.PbFieldType.O3)\n    ..aInt64(\n        4,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'time')\n    ..aOS(\n        5,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'maskUrl')\n    ..hasRequiredFields = false;\n\n  VideoMask._() : super();\n  factory VideoMask({\n    $fixnum.Int64? cid,\n    $core.int? plat,\n    $core.int? fps,\n    $fixnum.Int64? time,\n    $core.String? maskUrl,\n  }) {\n    final _result = create();\n    if (cid != null) {\n      _result.cid = cid;\n    }\n    if (plat != null) {\n      _result.plat = plat;\n    }\n    if (fps != null) {\n      _result.fps = fps;\n    }\n    if (time != null) {\n      _result.time = time;\n    }\n    if (maskUrl != null) {\n      _result.maskUrl = maskUrl;\n    }\n    return _result;\n  }\n  factory VideoMask.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory VideoMask.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  VideoMask clone() => VideoMask()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  VideoMask copyWith(void Function(VideoMask) updates) =>\n      super.copyWith((message) => updates(message as VideoMask))\n          as VideoMask; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static VideoMask create() => VideoMask._();\n  VideoMask createEmptyInstance() => create();\n  static $pb.PbList<VideoMask> createRepeated() => $pb.PbList<VideoMask>();\n  @$core.pragma('dart2js:noInline')\n  static VideoMask getDefault() =>\n      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<VideoMask>(create);\n  static VideoMask? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $fixnum.Int64 get cid => $_getI64(0);\n  @$pb.TagNumber(1)\n  set cid($fixnum.Int64 v) {\n    $_setInt64(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasCid() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearCid() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.int get plat => $_getIZ(1);\n  @$pb.TagNumber(2)\n  set plat($core.int v) {\n    $_setSignedInt32(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasPlat() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearPlat() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.int get fps => $_getIZ(2);\n  @$pb.TagNumber(3)\n  set fps($core.int v) {\n    $_setSignedInt32(2, v);\n  }\n\n  @$pb.TagNumber(3)\n  $core.bool hasFps() => $_has(2);\n  @$pb.TagNumber(3)\n  void clearFps() => clearField(3);\n\n  @$pb.TagNumber(4)\n  $fixnum.Int64 get time => $_getI64(3);\n  @$pb.TagNumber(4)\n  set time($fixnum.Int64 v) {\n    $_setInt64(3, v);\n  }\n\n  @$pb.TagNumber(4)\n  $core.bool hasTime() => $_has(3);\n  @$pb.TagNumber(4)\n  void clearTime() => clearField(4);\n\n  @$pb.TagNumber(5)\n  $core.String get maskUrl => $_getSZ(4);\n  @$pb.TagNumber(5)\n  set maskUrl($core.String v) {\n    $_setString(4, v);\n  }\n\n  @$pb.TagNumber(5)\n  $core.bool hasMaskUrl() => $_has(4);\n  @$pb.TagNumber(5)\n  void clearMaskUrl() => clearField(5);\n}\n\nclass VideoSubtitle extends $pb.GeneratedMessage {\n  static final $pb.BuilderInfo _i = $pb.BuilderInfo(\n      const $core.bool.fromEnvironment('protobuf.omit_message_names')\n          ? ''\n          : 'VideoSubtitle',\n      package: const $pb.PackageName(\n          const $core.bool.fromEnvironment('protobuf.omit_message_names')\n              ? ''\n              : 'bilibili.community.service.dm.v1'),\n      createEmptyInstance: create)\n    ..aOS(\n        1,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'lan')\n    ..aOS(\n        2,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'lanDoc',\n        protoName: 'lanDoc')\n    ..pc<SubtitleItem>(\n        3,\n        const $core.bool.fromEnvironment('protobuf.omit_field_names')\n            ? ''\n            : 'subtitles',\n        $pb.PbFieldType.PM,\n        subBuilder: SubtitleItem.create)\n    ..hasRequiredFields = false;\n\n  VideoSubtitle._() : super();\n  factory VideoSubtitle({\n    $core.String? lan,\n    $core.String? lanDoc,\n    $core.Iterable<SubtitleItem>? subtitles,\n  }) {\n    final _result = create();\n    if (lan != null) {\n      _result.lan = lan;\n    }\n    if (lanDoc != null) {\n      _result.lanDoc = lanDoc;\n    }\n    if (subtitles != null) {\n      _result.subtitles.addAll(subtitles);\n    }\n    return _result;\n  }\n  factory VideoSubtitle.fromBuffer($core.List<$core.int> i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromBuffer(i, r);\n  factory VideoSubtitle.fromJson($core.String i,\n          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>\n      create()..mergeFromJson(i, r);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '\n      'Will be removed in next major version')\n  VideoSubtitle clone() => VideoSubtitle()..mergeFromMessage(this);\n  @$core.Deprecated('Using this can add significant overhead to your binary. '\n      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '\n      'Will be removed in next major version')\n  VideoSubtitle copyWith(void Function(VideoSubtitle) updates) =>\n      super.copyWith((message) => updates(message as VideoSubtitle))\n          as VideoSubtitle; // ignore: deprecated_member_use\n  $pb.BuilderInfo get info_ => _i;\n  @$core.pragma('dart2js:noInline')\n  static VideoSubtitle create() => VideoSubtitle._();\n  VideoSubtitle createEmptyInstance() => create();\n  static $pb.PbList<VideoSubtitle> createRepeated() =>\n      $pb.PbList<VideoSubtitle>();\n  @$core.pragma('dart2js:noInline')\n  static VideoSubtitle getDefault() => _defaultInstance ??=\n      $pb.GeneratedMessage.$_defaultFor<VideoSubtitle>(create);\n  static VideoSubtitle? _defaultInstance;\n\n  @$pb.TagNumber(1)\n  $core.String get lan => $_getSZ(0);\n  @$pb.TagNumber(1)\n  set lan($core.String v) {\n    $_setString(0, v);\n  }\n\n  @$pb.TagNumber(1)\n  $core.bool hasLan() => $_has(0);\n  @$pb.TagNumber(1)\n  void clearLan() => clearField(1);\n\n  @$pb.TagNumber(2)\n  $core.String get lanDoc => $_getSZ(1);\n  @$pb.TagNumber(2)\n  set lanDoc($core.String v) {\n    $_setString(1, v);\n  }\n\n  @$pb.TagNumber(2)\n  $core.bool hasLanDoc() => $_has(1);\n  @$pb.TagNumber(2)\n  void clearLanDoc() => clearField(2);\n\n  @$pb.TagNumber(3)\n  $core.List<SubtitleItem> get subtitles => $_getList(2);\n}\n\nclass DMApi {\n  $pb.RpcClient _client;\n  DMApi(this._client);\n\n  $async.Future<DmSegMobileReply> dmSegMobile(\n      $pb.ClientContext? ctx, DmSegMobileReq request) {\n    var emptyResponse = DmSegMobileReply();\n    return _client.invoke<DmSegMobileReply>(\n        ctx, 'DM', 'DmSegMobile', request, emptyResponse);\n  }\n\n  $async.Future<DmViewReply> dmView($pb.ClientContext? ctx, DmViewReq request) {\n    var emptyResponse = DmViewReply();\n    return _client.invoke<DmViewReply>(\n        ctx, 'DM', 'DmView', request, emptyResponse);\n  }\n\n  $async.Future<Response> dmPlayerConfig(\n      $pb.ClientContext? ctx, DmPlayerConfigReq request) {\n    var emptyResponse = Response();\n    return _client.invoke<Response>(\n        ctx, 'DM', 'DmPlayerConfig', request, emptyResponse);\n  }\n\n  $async.Future<DmSegOttReply> dmSegOtt(\n      $pb.ClientContext? ctx, DmSegOttReq request) {\n    var emptyResponse = DmSegOttReply();\n    return _client.invoke<DmSegOttReply>(\n        ctx, 'DM', 'DmSegOtt', request, emptyResponse);\n  }\n\n  $async.Future<DmSegSDKReply> dmSegSDK(\n      $pb.ClientContext? ctx, DmSegSDKReq request) {\n    var emptyResponse = DmSegSDKReply();\n    return _client.invoke<DmSegSDKReply>(\n        ctx, 'DM', 'DmSegSDK', request, emptyResponse);\n  }\n\n  $async.Future<DmExpoReportRes> dmExpoReport(\n      $pb.ClientContext? ctx, DmExpoReportReq request) {\n    var emptyResponse = DmExpoReportRes();\n    return _client.invoke<DmExpoReportRes>(\n        ctx, 'DM', 'DmExpoReport', request, emptyResponse);\n  }\n}\n"
  },
  {
    "path": "lib/models/danmaku/dm.pbenum.dart",
    "content": "///\n//  Generated code. Do not modify.\n//  source: dm.proto\n//\n// @dart = 2.12\n// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name\n\n// ignore_for_file: UNDEFINED_SHOWN_NAME\nimport 'dart:core' as $core;\nimport 'package:protobuf/protobuf.dart' as $pb;\n\nclass AvatarType extends $pb.ProtobufEnum {\n  static const AvatarType AvatarTypeNone = AvatarType._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'AvatarTypeNone');\n  static const AvatarType AvatarTypeNFT = AvatarType._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'AvatarTypeNFT');\n\n  static const $core.List<AvatarType> values = <AvatarType>[\n    AvatarTypeNone,\n    AvatarTypeNFT,\n  ];\n\n  static final $core.Map<$core.int, AvatarType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static AvatarType? valueOf($core.int value) => _byValue[value];\n\n  const AvatarType._($core.int v, $core.String n) : super(v, n);\n}\n\nclass BubbleType extends $pb.ProtobufEnum {\n  static const BubbleType BubbleTypeNone = BubbleType._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'BubbleTypeNone');\n  static const BubbleType BubbleTypeClickButton = BubbleType._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'BubbleTypeClickButton');\n  static const BubbleType BubbleTypeDmSettingPanel = BubbleType._(\n      2,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'BubbleTypeDmSettingPanel');\n\n  static const $core.List<BubbleType> values = <BubbleType>[\n    BubbleTypeNone,\n    BubbleTypeClickButton,\n    BubbleTypeDmSettingPanel,\n  ];\n\n  static final $core.Map<$core.int, BubbleType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static BubbleType? valueOf($core.int value) => _byValue[value];\n\n  const BubbleType._($core.int v, $core.String n) : super(v, n);\n}\n\nclass CheckboxType extends $pb.ProtobufEnum {\n  static const CheckboxType CheckboxTypeNone = CheckboxType._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'CheckboxTypeNone');\n  static const CheckboxType CheckboxTypeEncourage = CheckboxType._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'CheckboxTypeEncourage');\n  static const CheckboxType CheckboxTypeColorDM = CheckboxType._(\n      2,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'CheckboxTypeColorDM');\n\n  static const $core.List<CheckboxType> values = <CheckboxType>[\n    CheckboxTypeNone,\n    CheckboxTypeEncourage,\n    CheckboxTypeColorDM,\n  ];\n\n  static final $core.Map<$core.int, CheckboxType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static CheckboxType? valueOf($core.int value) => _byValue[value];\n\n  const CheckboxType._($core.int v, $core.String n) : super(v, n);\n}\n\nclass DMAttrBit extends $pb.ProtobufEnum {\n  static const DMAttrBit DMAttrBitProtect = DMAttrBit._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'DMAttrBitProtect');\n  static const DMAttrBit DMAttrBitFromLive = DMAttrBit._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'DMAttrBitFromLive');\n  static const DMAttrBit DMAttrHighLike = DMAttrBit._(\n      2,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'DMAttrHighLike');\n\n  static const $core.List<DMAttrBit> values = <DMAttrBit>[\n    DMAttrBitProtect,\n    DMAttrBitFromLive,\n    DMAttrHighLike,\n  ];\n\n  static final $core.Map<$core.int, DMAttrBit> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static DMAttrBit? valueOf($core.int value) => _byValue[value];\n\n  const DMAttrBit._($core.int v, $core.String n) : super(v, n);\n}\n\nclass ExposureType extends $pb.ProtobufEnum {\n  static const ExposureType ExposureTypeNone = ExposureType._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'ExposureTypeNone');\n  static const ExposureType ExposureTypeDMSend = ExposureType._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'ExposureTypeDMSend');\n\n  static const $core.List<ExposureType> values = <ExposureType>[\n    ExposureTypeNone,\n    ExposureTypeDMSend,\n  ];\n\n  static final $core.Map<$core.int, ExposureType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static ExposureType? valueOf($core.int value) => _byValue[value];\n\n  const ExposureType._($core.int v, $core.String n) : super(v, n);\n}\n\nclass PostPanelBizType extends $pb.ProtobufEnum {\n  static const PostPanelBizType PostPanelBizTypeNone = PostPanelBizType._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'PostPanelBizTypeNone');\n  static const PostPanelBizType PostPanelBizTypeEncourage = PostPanelBizType._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'PostPanelBizTypeEncourage');\n  static const PostPanelBizType PostPanelBizTypeColorDM = PostPanelBizType._(\n      2,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'PostPanelBizTypeColorDM');\n  static const PostPanelBizType PostPanelBizTypeNFTDM = PostPanelBizType._(\n      3,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'PostPanelBizTypeNFTDM');\n  static const PostPanelBizType PostPanelBizTypeFragClose = PostPanelBizType._(\n      4,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'PostPanelBizTypeFragClose');\n  static const PostPanelBizType PostPanelBizTypeRecommend = PostPanelBizType._(\n      5,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'PostPanelBizTypeRecommend');\n\n  static const $core.List<PostPanelBizType> values = <PostPanelBizType>[\n    PostPanelBizTypeNone,\n    PostPanelBizTypeEncourage,\n    PostPanelBizTypeColorDM,\n    PostPanelBizTypeNFTDM,\n    PostPanelBizTypeFragClose,\n    PostPanelBizTypeRecommend,\n  ];\n\n  static final $core.Map<$core.int, PostPanelBizType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static PostPanelBizType? valueOf($core.int value) => _byValue[value];\n\n  const PostPanelBizType._($core.int v, $core.String n) : super(v, n);\n}\n\nclass PostStatus extends $pb.ProtobufEnum {\n  static const PostStatus PostStatusNormal = PostStatus._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'PostStatusNormal');\n  static const PostStatus PostStatusClosed = PostStatus._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'PostStatusClosed');\n\n  static const $core.List<PostStatus> values = <PostStatus>[\n    PostStatusNormal,\n    PostStatusClosed,\n  ];\n\n  static final $core.Map<$core.int, PostStatus> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static PostStatus? valueOf($core.int value) => _byValue[value];\n\n  const PostStatus._($core.int v, $core.String n) : super(v, n);\n}\n\nclass RenderType extends $pb.ProtobufEnum {\n  static const RenderType RenderTypeNone = RenderType._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'RenderTypeNone');\n  static const RenderType RenderTypeSingle = RenderType._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'RenderTypeSingle');\n  static const RenderType RenderTypeRotation = RenderType._(\n      2,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'RenderTypeRotation');\n\n  static const $core.List<RenderType> values = <RenderType>[\n    RenderTypeNone,\n    RenderTypeSingle,\n    RenderTypeRotation,\n  ];\n\n  static final $core.Map<$core.int, RenderType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static RenderType? valueOf($core.int value) => _byValue[value];\n\n  const RenderType._($core.int v, $core.String n) : super(v, n);\n}\n\nclass SubtitleAiStatus extends $pb.ProtobufEnum {\n  static const SubtitleAiStatus None = SubtitleAiStatus._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'None');\n  static const SubtitleAiStatus Exposure = SubtitleAiStatus._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'Exposure');\n  static const SubtitleAiStatus Assist = SubtitleAiStatus._(\n      2,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'Assist');\n\n  static const $core.List<SubtitleAiStatus> values = <SubtitleAiStatus>[\n    None,\n    Exposure,\n    Assist,\n  ];\n\n  static final $core.Map<$core.int, SubtitleAiStatus> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static SubtitleAiStatus? valueOf($core.int value) => _byValue[value];\n\n  const SubtitleAiStatus._($core.int v, $core.String n) : super(v, n);\n}\n\nclass SubtitleAiType extends $pb.ProtobufEnum {\n  static const SubtitleAiType Normal = SubtitleAiType._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'Normal');\n  static const SubtitleAiType Translate = SubtitleAiType._(\n      1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'Translate');\n\n  static const $core.List<SubtitleAiType> values = <SubtitleAiType>[\n    Normal,\n    Translate,\n  ];\n\n  static final $core.Map<$core.int, SubtitleAiType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static SubtitleAiType? valueOf($core.int value) => _byValue[value];\n\n  const SubtitleAiType._($core.int v, $core.String n) : super(v, n);\n}\n\nclass SubtitleType extends $pb.ProtobufEnum {\n  static const SubtitleType CC = SubtitleType._(0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CC');\n  static const SubtitleType AI = SubtitleType._(1,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'AI');\n\n  static const $core.List<SubtitleType> values = <SubtitleType>[\n    CC,\n    AI,\n  ];\n\n  static final $core.Map<$core.int, SubtitleType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static SubtitleType? valueOf($core.int value) => _byValue[value];\n\n  const SubtitleType._($core.int v, $core.String n) : super(v, n);\n}\n\nclass ToastFunctionType extends $pb.ProtobufEnum {\n  static const ToastFunctionType ToastFunctionTypeNone = ToastFunctionType._(\n      0,\n      const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n          ? ''\n          : 'ToastFunctionTypeNone');\n  static const ToastFunctionType ToastFunctionTypePostPanel =\n      ToastFunctionType._(\n          1,\n          const $core.bool.fromEnvironment('protobuf.omit_enum_names')\n              ? ''\n              : 'ToastFunctionTypePostPanel');\n\n  static const $core.List<ToastFunctionType> values = <ToastFunctionType>[\n    ToastFunctionTypeNone,\n    ToastFunctionTypePostPanel,\n  ];\n\n  static final $core.Map<$core.int, ToastFunctionType> _byValue =\n      $pb.ProtobufEnum.initByValue(values);\n  static ToastFunctionType? valueOf($core.int value) => _byValue[value];\n\n  const ToastFunctionType._($core.int v, $core.String n) : super(v, n);\n}\n"
  },
  {
    "path": "lib/models/danmaku/dm.pbjson.dart",
    "content": "///\n//  Generated code. Do not modify.\n//  source: dm.proto\n//\n// @dart = 2.12\n// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name\n\nimport 'dart:core' as $core;\nimport 'dart:convert' as $convert;\nimport 'dart:typed_data' as $typed_data;\n\n@$core.Deprecated('Use avatarTypeDescriptor instead')\nconst AvatarType$json = const {\n  '1': 'AvatarType',\n  '2': const [\n    const {'1': 'AvatarTypeNone', '2': 0},\n    const {'1': 'AvatarTypeNFT', '2': 1},\n  ],\n};\n\n/// Descriptor for `AvatarType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List avatarTypeDescriptor = $convert.base64Decode(\n    'CgpBdmF0YXJUeXBlEhIKDkF2YXRhclR5cGVOb25lEAASEQoNQXZhdGFyVHlwZU5GVBAB');\n@$core.Deprecated('Use bubbleTypeDescriptor instead')\nconst BubbleType$json = const {\n  '1': 'BubbleType',\n  '2': const [\n    const {'1': 'BubbleTypeNone', '2': 0},\n    const {'1': 'BubbleTypeClickButton', '2': 1},\n    const {'1': 'BubbleTypeDmSettingPanel', '2': 2},\n  ],\n};\n\n/// Descriptor for `BubbleType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List bubbleTypeDescriptor = $convert.base64Decode(\n    'CgpCdWJibGVUeXBlEhIKDkJ1YmJsZVR5cGVOb25lEAASGQoVQnViYmxlVHlwZUNsaWNrQnV0dG9uEAESHAoYQnViYmxlVHlwZURtU2V0dGluZ1BhbmVsEAI=');\n@$core.Deprecated('Use checkboxTypeDescriptor instead')\nconst CheckboxType$json = const {\n  '1': 'CheckboxType',\n  '2': const [\n    const {'1': 'CheckboxTypeNone', '2': 0},\n    const {'1': 'CheckboxTypeEncourage', '2': 1},\n    const {'1': 'CheckboxTypeColorDM', '2': 2},\n  ],\n};\n\n/// Descriptor for `CheckboxType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List checkboxTypeDescriptor = $convert.base64Decode(\n    'CgxDaGVja2JveFR5cGUSFAoQQ2hlY2tib3hUeXBlTm9uZRAAEhkKFUNoZWNrYm94VHlwZUVuY291cmFnZRABEhcKE0NoZWNrYm94VHlwZUNvbG9yRE0QAg==');\n@$core.Deprecated('Use dMAttrBitDescriptor instead')\nconst DMAttrBit$json = const {\n  '1': 'DMAttrBit',\n  '2': const [\n    const {'1': 'DMAttrBitProtect', '2': 0},\n    const {'1': 'DMAttrBitFromLive', '2': 1},\n    const {'1': 'DMAttrHighLike', '2': 2},\n  ],\n};\n\n/// Descriptor for `DMAttrBit`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List dMAttrBitDescriptor = $convert.base64Decode(\n    'CglETUF0dHJCaXQSFAoQRE1BdHRyQml0UHJvdGVjdBAAEhUKEURNQXR0ckJpdEZyb21MaXZlEAESEgoORE1BdHRySGlnaExpa2UQAg==');\n@$core.Deprecated('Use exposureTypeDescriptor instead')\nconst ExposureType$json = const {\n  '1': 'ExposureType',\n  '2': const [\n    const {'1': 'ExposureTypeNone', '2': 0},\n    const {'1': 'ExposureTypeDMSend', '2': 1},\n  ],\n};\n\n/// Descriptor for `ExposureType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List exposureTypeDescriptor = $convert.base64Decode(\n    'CgxFeHBvc3VyZVR5cGUSFAoQRXhwb3N1cmVUeXBlTm9uZRAAEhYKEkV4cG9zdXJlVHlwZURNU2VuZBAB');\n@$core.Deprecated('Use postPanelBizTypeDescriptor instead')\nconst PostPanelBizType$json = const {\n  '1': 'PostPanelBizType',\n  '2': const [\n    const {'1': 'PostPanelBizTypeNone', '2': 0},\n    const {'1': 'PostPanelBizTypeEncourage', '2': 1},\n    const {'1': 'PostPanelBizTypeColorDM', '2': 2},\n    const {'1': 'PostPanelBizTypeNFTDM', '2': 3},\n    const {'1': 'PostPanelBizTypeFragClose', '2': 4},\n    const {'1': 'PostPanelBizTypeRecommend', '2': 5},\n  ],\n};\n\n/// Descriptor for `PostPanelBizType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List postPanelBizTypeDescriptor = $convert.base64Decode(\n    'ChBQb3N0UGFuZWxCaXpUeXBlEhgKFFBvc3RQYW5lbEJpelR5cGVOb25lEAASHQoZUG9zdFBhbmVsQml6VHlwZUVuY291cmFnZRABEhsKF1Bvc3RQYW5lbEJpelR5cGVDb2xvckRNEAISGQoVUG9zdFBhbmVsQml6VHlwZU5GVERNEAMSHQoZUG9zdFBhbmVsQml6VHlwZUZyYWdDbG9zZRAEEh0KGVBvc3RQYW5lbEJpelR5cGVSZWNvbW1lbmQQBQ==');\n@$core.Deprecated('Use postStatusDescriptor instead')\nconst PostStatus$json = const {\n  '1': 'PostStatus',\n  '2': const [\n    const {'1': 'PostStatusNormal', '2': 0},\n    const {'1': 'PostStatusClosed', '2': 1},\n  ],\n};\n\n/// Descriptor for `PostStatus`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List postStatusDescriptor = $convert.base64Decode(\n    'CgpQb3N0U3RhdHVzEhQKEFBvc3RTdGF0dXNOb3JtYWwQABIUChBQb3N0U3RhdHVzQ2xvc2VkEAE=');\n@$core.Deprecated('Use renderTypeDescriptor instead')\nconst RenderType$json = const {\n  '1': 'RenderType',\n  '2': const [\n    const {'1': 'RenderTypeNone', '2': 0},\n    const {'1': 'RenderTypeSingle', '2': 1},\n    const {'1': 'RenderTypeRotation', '2': 2},\n  ],\n};\n\n/// Descriptor for `RenderType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List renderTypeDescriptor = $convert.base64Decode(\n    'CgpSZW5kZXJUeXBlEhIKDlJlbmRlclR5cGVOb25lEAASFAoQUmVuZGVyVHlwZVNpbmdsZRABEhYKElJlbmRlclR5cGVSb3RhdGlvbhAC');\n@$core.Deprecated('Use subtitleAiStatusDescriptor instead')\nconst SubtitleAiStatus$json = const {\n  '1': 'SubtitleAiStatus',\n  '2': const [\n    const {'1': 'None', '2': 0},\n    const {'1': 'Exposure', '2': 1},\n    const {'1': 'Assist', '2': 2},\n  ],\n};\n\n/// Descriptor for `SubtitleAiStatus`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List subtitleAiStatusDescriptor = $convert.base64Decode(\n    'ChBTdWJ0aXRsZUFpU3RhdHVzEggKBE5vbmUQABIMCghFeHBvc3VyZRABEgoKBkFzc2lzdBAC');\n@$core.Deprecated('Use subtitleAiTypeDescriptor instead')\nconst SubtitleAiType$json = const {\n  '1': 'SubtitleAiType',\n  '2': const [\n    const {'1': 'Normal', '2': 0},\n    const {'1': 'Translate', '2': 1},\n  ],\n};\n\n/// Descriptor for `SubtitleAiType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List subtitleAiTypeDescriptor = $convert.base64Decode(\n    'Cg5TdWJ0aXRsZUFpVHlwZRIKCgZOb3JtYWwQABINCglUcmFuc2xhdGUQAQ==');\n@$core.Deprecated('Use subtitleTypeDescriptor instead')\nconst SubtitleType$json = const {\n  '1': 'SubtitleType',\n  '2': const [\n    const {'1': 'CC', '2': 0},\n    const {'1': 'AI', '2': 1},\n  ],\n};\n\n/// Descriptor for `SubtitleType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List subtitleTypeDescriptor =\n    $convert.base64Decode('CgxTdWJ0aXRsZVR5cGUSBgoCQ0MQABIGCgJBSRAB');\n@$core.Deprecated('Use toastFunctionTypeDescriptor instead')\nconst ToastFunctionType$json = const {\n  '1': 'ToastFunctionType',\n  '2': const [\n    const {'1': 'ToastFunctionTypeNone', '2': 0},\n    const {'1': 'ToastFunctionTypePostPanel', '2': 1},\n  ],\n};\n\n/// Descriptor for `ToastFunctionType`. Decode as a `google.protobuf.EnumDescriptorProto`.\nfinal $typed_data.Uint8List toastFunctionTypeDescriptor = $convert.base64Decode(\n    'ChFUb2FzdEZ1bmN0aW9uVHlwZRIZChVUb2FzdEZ1bmN0aW9uVHlwZU5vbmUQABIeChpUb2FzdEZ1bmN0aW9uVHlwZVBvc3RQYW5lbBAB');\n@$core.Deprecated('Use avatarDescriptor instead')\nconst Avatar$json = const {\n  '1': 'Avatar',\n  '2': const [\n    const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},\n    const {'1': 'url', '3': 2, '4': 1, '5': 9, '10': 'url'},\n    const {\n      '1': 'avatar_type',\n      '3': 3,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.AvatarType',\n      '10': 'avatarType'\n    },\n  ],\n};\n\n/// Descriptor for `Avatar`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List avatarDescriptor = $convert.base64Decode(\n    'CgZBdmF0YXISDgoCaWQYASABKAlSAmlkEhAKA3VybBgCIAEoCVIDdXJsEk0KC2F2YXRhcl90eXBlGAMgASgOMiwuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuQXZhdGFyVHlwZVIKYXZhdGFyVHlwZQ==');\n@$core.Deprecated('Use bubbleDescriptor instead')\nconst Bubble$json = const {\n  '1': 'Bubble',\n  '2': const [\n    const {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},\n    const {'1': 'url', '3': 2, '4': 1, '5': 9, '10': 'url'},\n  ],\n};\n\n/// Descriptor for `Bubble`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List bubbleDescriptor = $convert.base64Decode(\n    'CgZCdWJibGUSEgoEdGV4dBgBIAEoCVIEdGV4dBIQCgN1cmwYAiABKAlSA3VybA==');\n@$core.Deprecated('Use bubbleV2Descriptor instead')\nconst BubbleV2$json = const {\n  '1': 'BubbleV2',\n  '2': const [\n    const {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},\n    const {'1': 'url', '3': 2, '4': 1, '5': 9, '10': 'url'},\n    const {\n      '1': 'bubble_type',\n      '3': 3,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.BubbleType',\n      '10': 'bubbleType'\n    },\n    const {'1': 'exposure_once', '3': 4, '4': 1, '5': 8, '10': 'exposureOnce'},\n    const {\n      '1': 'exposure_type',\n      '3': 5,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.ExposureType',\n      '10': 'exposureType'\n    },\n  ],\n};\n\n/// Descriptor for `BubbleV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List bubbleV2Descriptor = $convert.base64Decode(\n    'CghCdWJibGVWMhISCgR0ZXh0GAEgASgJUgR0ZXh0EhAKA3VybBgCIAEoCVIDdXJsEk0KC2J1YmJsZV90eXBlGAMgASgOMiwuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuQnViYmxlVHlwZVIKYnViYmxlVHlwZRIjCg1leHBvc3VyZV9vbmNlGAQgASgIUgxleHBvc3VyZU9uY2USUwoNZXhwb3N1cmVfdHlwZRgFIAEoDjIuLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkV4cG9zdXJlVHlwZVIMZXhwb3N1cmVUeXBl');\n@$core.Deprecated('Use buttonDescriptor instead')\nconst Button$json = const {\n  '1': 'Button',\n  '2': const [\n    const {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},\n    const {'1': 'action', '3': 2, '4': 1, '5': 5, '10': 'action'},\n  ],\n};\n\n/// Descriptor for `Button`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List buttonDescriptor = $convert.base64Decode(\n    'CgZCdXR0b24SEgoEdGV4dBgBIAEoCVIEdGV4dBIWCgZhY3Rpb24YAiABKAVSBmFjdGlvbg==');\n@$core.Deprecated('Use buzzwordConfigDescriptor instead')\nconst BuzzwordConfig$json = const {\n  '1': 'BuzzwordConfig',\n  '2': const [\n    const {\n      '1': 'keywords',\n      '3': 1,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.BuzzwordShowConfig',\n      '10': 'keywords'\n    },\n  ],\n};\n\n/// Descriptor for `BuzzwordConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List buzzwordConfigDescriptor = $convert.base64Decode(\n    'Cg5CdXp6d29yZENvbmZpZxJQCghrZXl3b3JkcxgBIAMoCzI0LmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkJ1enp3b3JkU2hvd0NvbmZpZ1IIa2V5d29yZHM=');\n@$core.Deprecated('Use buzzwordShowConfigDescriptor instead')\nconst BuzzwordShowConfig$json = const {\n  '1': 'BuzzwordShowConfig',\n  '2': const [\n    const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},\n    const {'1': 'schema', '3': 2, '4': 1, '5': 9, '10': 'schema'},\n    const {'1': 'source', '3': 3, '4': 1, '5': 5, '10': 'source'},\n    const {'1': 'id', '3': 4, '4': 1, '5': 3, '10': 'id'},\n    const {'1': 'buzzword_id', '3': 5, '4': 1, '5': 3, '10': 'buzzwordId'},\n    const {'1': 'schema_type', '3': 6, '4': 1, '5': 5, '10': 'schemaType'},\n  ],\n};\n\n/// Descriptor for `BuzzwordShowConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List buzzwordShowConfigDescriptor = $convert.base64Decode(\n    'ChJCdXp6d29yZFNob3dDb25maWcSEgoEbmFtZRgBIAEoCVIEbmFtZRIWCgZzY2hlbWEYAiABKAlSBnNjaGVtYRIWCgZzb3VyY2UYAyABKAVSBnNvdXJjZRIOCgJpZBgEIAEoA1ICaWQSHwoLYnV6endvcmRfaWQYBSABKANSCmJ1enp3b3JkSWQSHwoLc2NoZW1hX3R5cGUYBiABKAVSCnNjaGVtYVR5cGU=');\n@$core.Deprecated('Use checkBoxDescriptor instead')\nconst CheckBox$json = const {\n  '1': 'CheckBox',\n  '2': const [\n    const {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},\n    const {\n      '1': 'type',\n      '3': 2,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.CheckboxType',\n      '10': 'type'\n    },\n    const {'1': 'default_value', '3': 3, '4': 1, '5': 8, '10': 'defaultValue'},\n    const {'1': 'show', '3': 4, '4': 1, '5': 8, '10': 'show'},\n  ],\n};\n\n/// Descriptor for `CheckBox`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List checkBoxDescriptor = $convert.base64Decode(\n    'CghDaGVja0JveBISCgR0ZXh0GAEgASgJUgR0ZXh0EkIKBHR5cGUYAiABKA4yLi5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5DaGVja2JveFR5cGVSBHR5cGUSIwoNZGVmYXVsdF92YWx1ZRgDIAEoCFIMZGVmYXVsdFZhbHVlEhIKBHNob3cYBCABKAhSBHNob3c=');\n@$core.Deprecated('Use checkBoxV2Descriptor instead')\nconst CheckBoxV2$json = const {\n  '1': 'CheckBoxV2',\n  '2': const [\n    const {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},\n    const {'1': 'type', '3': 2, '4': 1, '5': 5, '10': 'type'},\n    const {'1': 'default_value', '3': 3, '4': 1, '5': 8, '10': 'defaultValue'},\n  ],\n};\n\n/// Descriptor for `CheckBoxV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List checkBoxV2Descriptor = $convert.base64Decode(\n    'CgpDaGVja0JveFYyEhIKBHRleHQYASABKAlSBHRleHQSEgoEdHlwZRgCIAEoBVIEdHlwZRIjCg1kZWZhdWx0X3ZhbHVlGAMgASgIUgxkZWZhdWx0VmFsdWU=');\n@$core.Deprecated('Use clickButtonDescriptor instead')\nconst ClickButton$json = const {\n  '1': 'ClickButton',\n  '2': const [\n    const {'1': 'portrait_text', '3': 1, '4': 3, '5': 9, '10': 'portraitText'},\n    const {\n      '1': 'landscape_text',\n      '3': 2,\n      '4': 3,\n      '5': 9,\n      '10': 'landscapeText'\n    },\n    const {\n      '1': 'portrait_text_focus',\n      '3': 3,\n      '4': 3,\n      '5': 9,\n      '10': 'portraitTextFocus'\n    },\n    const {\n      '1': 'landscape_text_focus',\n      '3': 4,\n      '4': 3,\n      '5': 9,\n      '10': 'landscapeTextFocus'\n    },\n    const {\n      '1': 'render_type',\n      '3': 5,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.RenderType',\n      '10': 'renderType'\n    },\n    const {'1': 'show', '3': 6, '4': 1, '5': 8, '10': 'show'},\n    const {\n      '1': 'bubble',\n      '3': 7,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Bubble',\n      '10': 'bubble'\n    },\n  ],\n};\n\n/// Descriptor for `ClickButton`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List clickButtonDescriptor = $convert.base64Decode(\n    'CgtDbGlja0J1dHRvbhIjCg1wb3J0cmFpdF90ZXh0GAEgAygJUgxwb3J0cmFpdFRleHQSJQoObGFuZHNjYXBlX3RleHQYAiADKAlSDWxhbmRzY2FwZVRleHQSLgoTcG9ydHJhaXRfdGV4dF9mb2N1cxgDIAMoCVIRcG9ydHJhaXRUZXh0Rm9jdXMSMAoUbGFuZHNjYXBlX3RleHRfZm9jdXMYBCADKAlSEmxhbmRzY2FwZVRleHRGb2N1cxJNCgtyZW5kZXJfdHlwZRgFIAEoDjIsLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlJlbmRlclR5cGVSCnJlbmRlclR5cGUSEgoEc2hvdxgGIAEoCFIEc2hvdxJACgZidWJibGUYByABKAsyKC5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5CdWJibGVSBmJ1YmJsZQ==');\n@$core.Deprecated('Use clickButtonV2Descriptor instead')\nconst ClickButtonV2$json = const {\n  '1': 'ClickButtonV2',\n  '2': const [\n    const {'1': 'portrait_text', '3': 1, '4': 3, '5': 9, '10': 'portraitText'},\n    const {\n      '1': 'landscape_text',\n      '3': 2,\n      '4': 3,\n      '5': 9,\n      '10': 'landscapeText'\n    },\n    const {\n      '1': 'portrait_text_focus',\n      '3': 3,\n      '4': 3,\n      '5': 9,\n      '10': 'portraitTextFocus'\n    },\n    const {\n      '1': 'landscape_text_focus',\n      '3': 4,\n      '4': 3,\n      '5': 9,\n      '10': 'landscapeTextFocus'\n    },\n    const {'1': 'render_type', '3': 5, '4': 1, '5': 5, '10': 'renderType'},\n    const {\n      '1': 'text_input_post',\n      '3': 6,\n      '4': 1,\n      '5': 8,\n      '10': 'textInputPost'\n    },\n    const {'1': 'exposure_once', '3': 7, '4': 1, '5': 8, '10': 'exposureOnce'},\n    const {'1': 'exposure_type', '3': 8, '4': 1, '5': 5, '10': 'exposureType'},\n  ],\n};\n\n/// Descriptor for `ClickButtonV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List clickButtonV2Descriptor = $convert.base64Decode(\n    'Cg1DbGlja0J1dHRvblYyEiMKDXBvcnRyYWl0X3RleHQYASADKAlSDHBvcnRyYWl0VGV4dBIlCg5sYW5kc2NhcGVfdGV4dBgCIAMoCVINbGFuZHNjYXBlVGV4dBIuChNwb3J0cmFpdF90ZXh0X2ZvY3VzGAMgAygJUhFwb3J0cmFpdFRleHRGb2N1cxIwChRsYW5kc2NhcGVfdGV4dF9mb2N1cxgEIAMoCVISbGFuZHNjYXBlVGV4dEZvY3VzEh8KC3JlbmRlcl90eXBlGAUgASgFUgpyZW5kZXJUeXBlEiYKD3RleHRfaW5wdXRfcG9zdBgGIAEoCFINdGV4dElucHV0UG9zdBIjCg1leHBvc3VyZV9vbmNlGAcgASgIUgxleHBvc3VyZU9uY2USIwoNZXhwb3N1cmVfdHlwZRgIIAEoBVIMZXhwb3N1cmVUeXBl');\n@$core.Deprecated('Use commandDmDescriptor instead')\nconst CommandDm$json = const {\n  '1': 'CommandDm',\n  '2': const [\n    const {'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'},\n    const {'1': 'oid', '3': 2, '4': 1, '5': 3, '10': 'oid'},\n    const {'1': 'mid', '3': 3, '4': 1, '5': 9, '10': 'mid'},\n    const {'1': 'command', '3': 4, '4': 1, '5': 9, '10': 'command'},\n    const {'1': 'content', '3': 5, '4': 1, '5': 9, '10': 'content'},\n    const {'1': 'progress', '3': 6, '4': 1, '5': 5, '10': 'progress'},\n    const {'1': 'ctime', '3': 7, '4': 1, '5': 9, '10': 'ctime'},\n    const {'1': 'mtime', '3': 8, '4': 1, '5': 9, '10': 'mtime'},\n    const {'1': 'extra', '3': 9, '4': 1, '5': 9, '10': 'extra'},\n    const {'1': 'idStr', '3': 10, '4': 1, '5': 9, '10': 'idStr'},\n  ],\n};\n\n/// Descriptor for `CommandDm`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List commandDmDescriptor = $convert.base64Decode(\n    'CglDb21tYW5kRG0SDgoCaWQYASABKANSAmlkEhAKA29pZBgCIAEoA1IDb2lkEhAKA21pZBgDIAEoCVIDbWlkEhgKB2NvbW1hbmQYBCABKAlSB2NvbW1hbmQSGAoHY29udGVudBgFIAEoCVIHY29udGVudBIaCghwcm9ncmVzcxgGIAEoBVIIcHJvZ3Jlc3MSFAoFY3RpbWUYByABKAlSBWN0aW1lEhQKBW10aW1lGAggASgJUgVtdGltZRIUCgVleHRyYRgJIAEoCVIFZXh0cmESFAoFaWRTdHIYCiABKAlSBWlkU3Ry');\n@$core.Deprecated('Use danmakuAIFlagDescriptor instead')\nconst DanmakuAIFlag$json = const {\n  '1': 'DanmakuAIFlag',\n  '2': const [\n    const {\n      '1': 'dm_flags',\n      '3': 1,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmakuFlag',\n      '10': 'dmFlags'\n    },\n  ],\n};\n\n/// Descriptor for `DanmakuAIFlag`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmakuAIFlagDescriptor = $convert.base64Decode(\n    'Cg1EYW5tYWt1QUlGbGFnEkgKCGRtX2ZsYWdzGAEgAygLMi0uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRGFubWFrdUZsYWdSB2RtRmxhZ3M=');\n@$core.Deprecated('Use danmakuElemDescriptor instead')\nconst DanmakuElem$json = const {\n  '1': 'DanmakuElem',\n  '2': const [\n    const {'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'},\n    const {'1': 'progress', '3': 2, '4': 1, '5': 5, '10': 'progress'},\n    const {'1': 'mode', '3': 3, '4': 1, '5': 5, '10': 'mode'},\n    const {'1': 'fontsize', '3': 4, '4': 1, '5': 5, '10': 'fontsize'},\n    const {'1': 'color', '3': 5, '4': 1, '5': 13, '10': 'color'},\n    const {'1': 'midHash', '3': 6, '4': 1, '5': 9, '10': 'midHash'},\n    const {'1': 'content', '3': 7, '4': 1, '5': 9, '10': 'content'},\n    const {'1': 'ctime', '3': 8, '4': 1, '5': 3, '10': 'ctime'},\n    const {'1': 'weight', '3': 9, '4': 1, '5': 5, '10': 'weight'},\n    const {'1': 'action', '3': 10, '4': 1, '5': 9, '10': 'action'},\n    const {'1': 'pool', '3': 11, '4': 1, '5': 5, '10': 'pool'},\n    const {'1': 'idStr', '3': 12, '4': 1, '5': 9, '10': 'idStr'},\n    const {'1': 'attr', '3': 13, '4': 1, '5': 5, '10': 'attr'},\n    const {'1': 'animation', '3': 22, '4': 1, '5': 9, '10': 'animation'},\n  ],\n};\n\n/// Descriptor for `DanmakuElem`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmakuElemDescriptor = $convert.base64Decode(\n    'CgtEYW5tYWt1RWxlbRIOCgJpZBgBIAEoA1ICaWQSGgoIcHJvZ3Jlc3MYAiABKAVSCHByb2dyZXNzEhIKBG1vZGUYAyABKAVSBG1vZGUSGgoIZm9udHNpemUYBCABKAVSCGZvbnRzaXplEhQKBWNvbG9yGAUgASgNUgVjb2xvchIYCgdtaWRIYXNoGAYgASgJUgdtaWRIYXNoEhgKB2NvbnRlbnQYByABKAlSB2NvbnRlbnQSFAoFY3RpbWUYCCABKANSBWN0aW1lEhYKBndlaWdodBgJIAEoBVIGd2VpZ2h0EhYKBmFjdGlvbhgKIAEoCVIGYWN0aW9uEhIKBHBvb2wYCyABKAVSBHBvb2wSFAoFaWRTdHIYDCABKAlSBWlkU3RyEhIKBGF0dHIYDSABKAVSBGF0dHISHAoJYW5pbWF0aW9uGBYgASgJUglhbmltYXRpb24=');\n@$core.Deprecated('Use danmakuFlagDescriptor instead')\nconst DanmakuFlag$json = const {\n  '1': 'DanmakuFlag',\n  '2': const [\n    const {'1': 'dmid', '3': 1, '4': 1, '5': 3, '10': 'dmid'},\n    const {'1': 'flag', '3': 2, '4': 1, '5': 13, '10': 'flag'},\n  ],\n};\n\n/// Descriptor for `DanmakuFlag`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmakuFlagDescriptor = $convert.base64Decode(\n    'CgtEYW5tYWt1RmxhZxISCgRkbWlkGAEgASgDUgRkbWlkEhIKBGZsYWcYAiABKA1SBGZsYWc=');\n@$core.Deprecated('Use danmakuFlagConfigDescriptor instead')\nconst DanmakuFlagConfig$json = const {\n  '1': 'DanmakuFlagConfig',\n  '2': const [\n    const {'1': 'rec_flag', '3': 1, '4': 1, '5': 5, '10': 'recFlag'},\n    const {'1': 'rec_text', '3': 2, '4': 1, '5': 9, '10': 'recText'},\n    const {'1': 'rec_switch', '3': 3, '4': 1, '5': 5, '10': 'recSwitch'},\n  ],\n};\n\n/// Descriptor for `DanmakuFlagConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmakuFlagConfigDescriptor = $convert.base64Decode(\n    'ChFEYW5tYWt1RmxhZ0NvbmZpZxIZCghyZWNfZmxhZxgBIAEoBVIHcmVjRmxhZxIZCghyZWNfdGV4dBgCIAEoCVIHcmVjVGV4dBIdCgpyZWNfc3dpdGNoGAMgASgFUglyZWNTd2l0Y2g=');\n@$core.Deprecated('Use danmuDefaultPlayerConfigDescriptor instead')\nconst DanmuDefaultPlayerConfig$json = const {\n  '1': 'DanmuDefaultPlayerConfig',\n  '2': const [\n    const {\n      '1': 'player_danmaku_use_default_config',\n      '3': 1,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuUseDefaultConfig'\n    },\n    const {\n      '1': 'player_danmaku_ai_recommended_switch',\n      '3': 4,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuAiRecommendedSwitch'\n    },\n    const {\n      '1': 'player_danmaku_ai_recommended_level',\n      '3': 5,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuAiRecommendedLevel'\n    },\n    const {\n      '1': 'player_danmaku_blocktop',\n      '3': 6,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlocktop'\n    },\n    const {\n      '1': 'player_danmaku_blockscroll',\n      '3': 7,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockscroll'\n    },\n    const {\n      '1': 'player_danmaku_blockbottom',\n      '3': 8,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockbottom'\n    },\n    const {\n      '1': 'player_danmaku_blockcolorful',\n      '3': 9,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockcolorful'\n    },\n    const {\n      '1': 'player_danmaku_blockrepeat',\n      '3': 10,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockrepeat'\n    },\n    const {\n      '1': 'player_danmaku_blockspecial',\n      '3': 11,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockspecial'\n    },\n    const {\n      '1': 'player_danmaku_opacity',\n      '3': 12,\n      '4': 1,\n      '5': 2,\n      '10': 'playerDanmakuOpacity'\n    },\n    const {\n      '1': 'player_danmaku_scalingfactor',\n      '3': 13,\n      '4': 1,\n      '5': 2,\n      '10': 'playerDanmakuScalingfactor'\n    },\n    const {\n      '1': 'player_danmaku_domain',\n      '3': 14,\n      '4': 1,\n      '5': 2,\n      '10': 'playerDanmakuDomain'\n    },\n    const {\n      '1': 'player_danmaku_speed',\n      '3': 15,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuSpeed'\n    },\n    const {\n      '1': 'inline_player_danmaku_switch',\n      '3': 16,\n      '4': 1,\n      '5': 8,\n      '10': 'inlinePlayerDanmakuSwitch'\n    },\n    const {\n      '1': 'player_danmaku_senior_mode_switch',\n      '3': 17,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuSeniorModeSwitch'\n    },\n    const {\n      '1': 'player_danmaku_ai_recommended_level_v2',\n      '3': 18,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuAiRecommendedLevelV2'\n    },\n    const {\n      '1': 'player_danmaku_ai_recommended_level_v2_map',\n      '3': 19,\n      '4': 3,\n      '5': 11,\n      '6':\n          '.bilibili.community.service.dm.v1.DanmuDefaultPlayerConfig.PlayerDanmakuAiRecommendedLevelV2MapEntry',\n      '10': 'playerDanmakuAiRecommendedLevelV2Map'\n    },\n  ],\n  '3': const [\n    DanmuDefaultPlayerConfig_PlayerDanmakuAiRecommendedLevelV2MapEntry$json\n  ],\n};\n\n@$core.Deprecated('Use danmuDefaultPlayerConfigDescriptor instead')\nconst DanmuDefaultPlayerConfig_PlayerDanmakuAiRecommendedLevelV2MapEntry$json =\n    const {\n  '1': 'PlayerDanmakuAiRecommendedLevelV2MapEntry',\n  '2': const [\n    const {'1': 'key', '3': 1, '4': 1, '5': 5, '10': 'key'},\n    const {'1': 'value', '3': 2, '4': 1, '5': 5, '10': 'value'},\n  ],\n  '7': const {'7': true},\n};\n\n/// Descriptor for `DanmuDefaultPlayerConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmuDefaultPlayerConfigDescriptor =\n    $convert.base64Decode(\n        'ChhEYW5tdURlZmF1bHRQbGF5ZXJDb25maWcSSAohcGxheWVyX2Rhbm1ha3VfdXNlX2RlZmF1bHRfY29uZmlnGAEgASgIUh1wbGF5ZXJEYW5tYWt1VXNlRGVmYXVsdENvbmZpZxJOCiRwbGF5ZXJfZGFubWFrdV9haV9yZWNvbW1lbmRlZF9zd2l0Y2gYBCABKAhSIHBsYXllckRhbm1ha3VBaVJlY29tbWVuZGVkU3dpdGNoEkwKI3BsYXllcl9kYW5tYWt1X2FpX3JlY29tbWVuZGVkX2xldmVsGAUgASgFUh9wbGF5ZXJEYW5tYWt1QWlSZWNvbW1lbmRlZExldmVsEjYKF3BsYXllcl9kYW5tYWt1X2Jsb2NrdG9wGAYgASgIUhVwbGF5ZXJEYW5tYWt1QmxvY2t0b3ASPAoacGxheWVyX2Rhbm1ha3VfYmxvY2tzY3JvbGwYByABKAhSGHBsYXllckRhbm1ha3VCbG9ja3Njcm9sbBI8ChpwbGF5ZXJfZGFubWFrdV9ibG9ja2JvdHRvbRgIIAEoCFIYcGxheWVyRGFubWFrdUJsb2NrYm90dG9tEkAKHHBsYXllcl9kYW5tYWt1X2Jsb2NrY29sb3JmdWwYCSABKAhSGnBsYXllckRhbm1ha3VCbG9ja2NvbG9yZnVsEjwKGnBsYXllcl9kYW5tYWt1X2Jsb2NrcmVwZWF0GAogASgIUhhwbGF5ZXJEYW5tYWt1QmxvY2tyZXBlYXQSPgobcGxheWVyX2Rhbm1ha3VfYmxvY2tzcGVjaWFsGAsgASgIUhlwbGF5ZXJEYW5tYWt1QmxvY2tzcGVjaWFsEjQKFnBsYXllcl9kYW5tYWt1X29wYWNpdHkYDCABKAJSFHBsYXllckRhbm1ha3VPcGFjaXR5EkAKHHBsYXllcl9kYW5tYWt1X3NjYWxpbmdmYWN0b3IYDSABKAJSGnBsYXllckRhbm1ha3VTY2FsaW5nZmFjdG9yEjIKFXBsYXllcl9kYW5tYWt1X2RvbWFpbhgOIAEoAlITcGxheWVyRGFubWFrdURvbWFpbhIwChRwbGF5ZXJfZGFubWFrdV9zcGVlZBgPIAEoBVIScGxheWVyRGFubWFrdVNwZWVkEj8KHGlubGluZV9wbGF5ZXJfZGFubWFrdV9zd2l0Y2gYECABKAhSGWlubGluZVBsYXllckRhbm1ha3VTd2l0Y2gSSAohcGxheWVyX2Rhbm1ha3Vfc2VuaW9yX21vZGVfc3dpdGNoGBEgASgFUh1wbGF5ZXJEYW5tYWt1U2VuaW9yTW9kZVN3aXRjaBJRCiZwbGF5ZXJfZGFubWFrdV9haV9yZWNvbW1lbmRlZF9sZXZlbF92MhgSIAEoBVIhcGxheWVyRGFubWFrdUFpUmVjb21tZW5kZWRMZXZlbFYyEr4BCipwbGF5ZXJfZGFubWFrdV9haV9yZWNvbW1lbmRlZF9sZXZlbF92Ml9tYXAYEyADKAsyZC5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5EYW5tdURlZmF1bHRQbGF5ZXJDb25maWcuUGxheWVyRGFubWFrdUFpUmVjb21tZW5kZWRMZXZlbFYyTWFwRW50cnlSJHBsYXllckRhbm1ha3VBaVJlY29tbWVuZGVkTGV2ZWxWMk1hcBpXCilQbGF5ZXJEYW5tYWt1QWlSZWNvbW1lbmRlZExldmVsVjJNYXBFbnRyeRIQCgNrZXkYASABKAVSA2tleRIUCgV2YWx1ZRgCIAEoBVIFdmFsdWU6AjgB');\n@$core.Deprecated('Use danmuPlayerConfigDescriptor instead')\nconst DanmuPlayerConfig$json = const {\n  '1': 'DanmuPlayerConfig',\n  '2': const [\n    const {\n      '1': 'player_danmaku_switch',\n      '3': 1,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuSwitch'\n    },\n    const {\n      '1': 'player_danmaku_switch_save',\n      '3': 2,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuSwitchSave'\n    },\n    const {\n      '1': 'player_danmaku_use_default_config',\n      '3': 3,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuUseDefaultConfig'\n    },\n    const {\n      '1': 'player_danmaku_ai_recommended_switch',\n      '3': 4,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuAiRecommendedSwitch'\n    },\n    const {\n      '1': 'player_danmaku_ai_recommended_level',\n      '3': 5,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuAiRecommendedLevel'\n    },\n    const {\n      '1': 'player_danmaku_blocktop',\n      '3': 6,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlocktop'\n    },\n    const {\n      '1': 'player_danmaku_blockscroll',\n      '3': 7,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockscroll'\n    },\n    const {\n      '1': 'player_danmaku_blockbottom',\n      '3': 8,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockbottom'\n    },\n    const {\n      '1': 'player_danmaku_blockcolorful',\n      '3': 9,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockcolorful'\n    },\n    const {\n      '1': 'player_danmaku_blockrepeat',\n      '3': 10,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockrepeat'\n    },\n    const {\n      '1': 'player_danmaku_blockspecial',\n      '3': 11,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuBlockspecial'\n    },\n    const {\n      '1': 'player_danmaku_opacity',\n      '3': 12,\n      '4': 1,\n      '5': 2,\n      '10': 'playerDanmakuOpacity'\n    },\n    const {\n      '1': 'player_danmaku_scalingfactor',\n      '3': 13,\n      '4': 1,\n      '5': 2,\n      '10': 'playerDanmakuScalingfactor'\n    },\n    const {\n      '1': 'player_danmaku_domain',\n      '3': 14,\n      '4': 1,\n      '5': 2,\n      '10': 'playerDanmakuDomain'\n    },\n    const {\n      '1': 'player_danmaku_speed',\n      '3': 15,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuSpeed'\n    },\n    const {\n      '1': 'player_danmaku_enableblocklist',\n      '3': 16,\n      '4': 1,\n      '5': 8,\n      '10': 'playerDanmakuEnableblocklist'\n    },\n    const {\n      '1': 'inline_player_danmaku_switch',\n      '3': 17,\n      '4': 1,\n      '5': 8,\n      '10': 'inlinePlayerDanmakuSwitch'\n    },\n    const {\n      '1': 'inline_player_danmaku_config',\n      '3': 18,\n      '4': 1,\n      '5': 5,\n      '10': 'inlinePlayerDanmakuConfig'\n    },\n    const {\n      '1': 'player_danmaku_ios_switch_save',\n      '3': 19,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuIosSwitchSave'\n    },\n    const {\n      '1': 'player_danmaku_senior_mode_switch',\n      '3': 20,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuSeniorModeSwitch'\n    },\n    const {\n      '1': 'player_danmaku_ai_recommended_level_v2',\n      '3': 21,\n      '4': 1,\n      '5': 5,\n      '10': 'playerDanmakuAiRecommendedLevelV2'\n    },\n    const {\n      '1': 'player_danmaku_ai_recommended_level_v2_map',\n      '3': 22,\n      '4': 3,\n      '5': 11,\n      '6':\n          '.bilibili.community.service.dm.v1.DanmuPlayerConfig.PlayerDanmakuAiRecommendedLevelV2MapEntry',\n      '10': 'playerDanmakuAiRecommendedLevelV2Map'\n    },\n  ],\n  '3': const [DanmuPlayerConfig_PlayerDanmakuAiRecommendedLevelV2MapEntry$json],\n};\n\n@$core.Deprecated('Use danmuPlayerConfigDescriptor instead')\nconst DanmuPlayerConfig_PlayerDanmakuAiRecommendedLevelV2MapEntry$json = const {\n  '1': 'PlayerDanmakuAiRecommendedLevelV2MapEntry',\n  '2': const [\n    const {'1': 'key', '3': 1, '4': 1, '5': 5, '10': 'key'},\n    const {'1': 'value', '3': 2, '4': 1, '5': 5, '10': 'value'},\n  ],\n  '7': const {'7': true},\n};\n\n/// Descriptor for `DanmuPlayerConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmuPlayerConfigDescriptor = $convert.base64Decode(\n    'ChFEYW5tdVBsYXllckNvbmZpZxIyChVwbGF5ZXJfZGFubWFrdV9zd2l0Y2gYASABKAhSE3BsYXllckRhbm1ha3VTd2l0Y2gSOwoacGxheWVyX2Rhbm1ha3Vfc3dpdGNoX3NhdmUYAiABKAhSF3BsYXllckRhbm1ha3VTd2l0Y2hTYXZlEkgKIXBsYXllcl9kYW5tYWt1X3VzZV9kZWZhdWx0X2NvbmZpZxgDIAEoCFIdcGxheWVyRGFubWFrdVVzZURlZmF1bHRDb25maWcSTgokcGxheWVyX2Rhbm1ha3VfYWlfcmVjb21tZW5kZWRfc3dpdGNoGAQgASgIUiBwbGF5ZXJEYW5tYWt1QWlSZWNvbW1lbmRlZFN3aXRjaBJMCiNwbGF5ZXJfZGFubWFrdV9haV9yZWNvbW1lbmRlZF9sZXZlbBgFIAEoBVIfcGxheWVyRGFubWFrdUFpUmVjb21tZW5kZWRMZXZlbBI2ChdwbGF5ZXJfZGFubWFrdV9ibG9ja3RvcBgGIAEoCFIVcGxheWVyRGFubWFrdUJsb2NrdG9wEjwKGnBsYXllcl9kYW5tYWt1X2Jsb2Nrc2Nyb2xsGAcgASgIUhhwbGF5ZXJEYW5tYWt1QmxvY2tzY3JvbGwSPAoacGxheWVyX2Rhbm1ha3VfYmxvY2tib3R0b20YCCABKAhSGHBsYXllckRhbm1ha3VCbG9ja2JvdHRvbRJAChxwbGF5ZXJfZGFubWFrdV9ibG9ja2NvbG9yZnVsGAkgASgIUhpwbGF5ZXJEYW5tYWt1QmxvY2tjb2xvcmZ1bBI8ChpwbGF5ZXJfZGFubWFrdV9ibG9ja3JlcGVhdBgKIAEoCFIYcGxheWVyRGFubWFrdUJsb2NrcmVwZWF0Ej4KG3BsYXllcl9kYW5tYWt1X2Jsb2Nrc3BlY2lhbBgLIAEoCFIZcGxheWVyRGFubWFrdUJsb2Nrc3BlY2lhbBI0ChZwbGF5ZXJfZGFubWFrdV9vcGFjaXR5GAwgASgCUhRwbGF5ZXJEYW5tYWt1T3BhY2l0eRJAChxwbGF5ZXJfZGFubWFrdV9zY2FsaW5nZmFjdG9yGA0gASgCUhpwbGF5ZXJEYW5tYWt1U2NhbGluZ2ZhY3RvchIyChVwbGF5ZXJfZGFubWFrdV9kb21haW4YDiABKAJSE3BsYXllckRhbm1ha3VEb21haW4SMAoUcGxheWVyX2Rhbm1ha3Vfc3BlZWQYDyABKAVSEnBsYXllckRhbm1ha3VTcGVlZBJECh5wbGF5ZXJfZGFubWFrdV9lbmFibGVibG9ja2xpc3QYECABKAhSHHBsYXllckRhbm1ha3VFbmFibGVibG9ja2xpc3QSPwocaW5saW5lX3BsYXllcl9kYW5tYWt1X3N3aXRjaBgRIAEoCFIZaW5saW5lUGxheWVyRGFubWFrdVN3aXRjaBI/ChxpbmxpbmVfcGxheWVyX2Rhbm1ha3VfY29uZmlnGBIgASgFUhlpbmxpbmVQbGF5ZXJEYW5tYWt1Q29uZmlnEkIKHnBsYXllcl9kYW5tYWt1X2lvc19zd2l0Y2hfc2F2ZRgTIAEoBVIacGxheWVyRGFubWFrdUlvc1N3aXRjaFNhdmUSSAohcGxheWVyX2Rhbm1ha3Vfc2VuaW9yX21vZGVfc3dpdGNoGBQgASgFUh1wbGF5ZXJEYW5tYWt1U2VuaW9yTW9kZVN3aXRjaBJRCiZwbGF5ZXJfZGFubWFrdV9haV9yZWNvbW1lbmRlZF9sZXZlbF92MhgVIAEoBVIhcGxheWVyRGFubWFrdUFpUmVjb21tZW5kZWRMZXZlbFYyErcBCipwbGF5ZXJfZGFubWFrdV9haV9yZWNvbW1lbmRlZF9sZXZlbF92Ml9tYXAYFiADKAsyXS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5EYW5tdVBsYXllckNvbmZpZy5QbGF5ZXJEYW5tYWt1QWlSZWNvbW1lbmRlZExldmVsVjJNYXBFbnRyeVIkcGxheWVyRGFubWFrdUFpUmVjb21tZW5kZWRMZXZlbFYyTWFwGlcKKVBsYXllckRhbm1ha3VBaVJlY29tbWVuZGVkTGV2ZWxWMk1hcEVudHJ5EhAKA2tleRgBIAEoBVIDa2V5EhQKBXZhbHVlGAIgASgFUgV2YWx1ZToCOAE=');\n@$core.Deprecated('Use danmuPlayerConfigPanelDescriptor instead')\nconst DanmuPlayerConfigPanel$json = const {\n  '1': 'DanmuPlayerConfigPanel',\n  '2': const [\n    const {\n      '1': 'selection_text',\n      '3': 1,\n      '4': 1,\n      '5': 9,\n      '10': 'selectionText'\n    },\n  ],\n};\n\n/// Descriptor for `DanmuPlayerConfigPanel`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmuPlayerConfigPanelDescriptor =\n    $convert.base64Decode(\n        'ChZEYW5tdVBsYXllckNvbmZpZ1BhbmVsEiUKDnNlbGVjdGlvbl90ZXh0GAEgASgJUg1zZWxlY3Rpb25UZXh0');\n@$core.Deprecated('Use danmuPlayerDynamicConfigDescriptor instead')\nconst DanmuPlayerDynamicConfig$json = const {\n  '1': 'DanmuPlayerDynamicConfig',\n  '2': const [\n    const {'1': 'progress', '3': 1, '4': 1, '5': 5, '10': 'progress'},\n    const {\n      '1': 'player_danmaku_domain',\n      '3': 14,\n      '4': 1,\n      '5': 2,\n      '10': 'playerDanmakuDomain'\n    },\n  ],\n};\n\n/// Descriptor for `DanmuPlayerDynamicConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmuPlayerDynamicConfigDescriptor =\n    $convert.base64Decode(\n        'ChhEYW5tdVBsYXllckR5bmFtaWNDb25maWcSGgoIcHJvZ3Jlc3MYASABKAVSCHByb2dyZXNzEjIKFXBsYXllcl9kYW5tYWt1X2RvbWFpbhgOIAEoAlITcGxheWVyRGFubWFrdURvbWFpbg==');\n@$core.Deprecated('Use danmuPlayerViewConfigDescriptor instead')\nconst DanmuPlayerViewConfig$json = const {\n  '1': 'DanmuPlayerViewConfig',\n  '2': const [\n    const {\n      '1': 'danmuku_default_player_config',\n      '3': 1,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmuDefaultPlayerConfig',\n      '10': 'danmukuDefaultPlayerConfig'\n    },\n    const {\n      '1': 'danmuku_player_config',\n      '3': 2,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmuPlayerConfig',\n      '10': 'danmukuPlayerConfig'\n    },\n    const {\n      '1': 'danmuku_player_dynamic_config',\n      '3': 3,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmuPlayerDynamicConfig',\n      '10': 'danmukuPlayerDynamicConfig'\n    },\n    const {\n      '1': 'danmuku_player_config_panel',\n      '3': 4,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmuPlayerConfigPanel',\n      '10': 'danmukuPlayerConfigPanel'\n    },\n  ],\n};\n\n/// Descriptor for `DanmuPlayerViewConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmuPlayerViewConfigDescriptor = $convert.base64Decode(\n    'ChVEYW5tdVBsYXllclZpZXdDb25maWcSfQodZGFubXVrdV9kZWZhdWx0X3BsYXllcl9jb25maWcYASABKAsyOi5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5EYW5tdURlZmF1bHRQbGF5ZXJDb25maWdSGmRhbm11a3VEZWZhdWx0UGxheWVyQ29uZmlnEmcKFWRhbm11a3VfcGxheWVyX2NvbmZpZxgCIAEoCzIzLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRhbm11UGxheWVyQ29uZmlnUhNkYW5tdWt1UGxheWVyQ29uZmlnEn0KHWRhbm11a3VfcGxheWVyX2R5bmFtaWNfY29uZmlnGAMgAygLMjouYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRGFubXVQbGF5ZXJEeW5hbWljQ29uZmlnUhpkYW5tdWt1UGxheWVyRHluYW1pY0NvbmZpZxJ3ChtkYW5tdWt1X3BsYXllcl9jb25maWdfcGFuZWwYBCABKAsyOC5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5EYW5tdVBsYXllckNvbmZpZ1BhbmVsUhhkYW5tdWt1UGxheWVyQ29uZmlnUGFuZWw=');\n@$core.Deprecated('Use danmuWebPlayerConfigDescriptor instead')\nconst DanmuWebPlayerConfig$json = const {\n  '1': 'DanmuWebPlayerConfig',\n  '2': const [\n    const {'1': 'dm_switch', '3': 1, '4': 1, '5': 8, '10': 'dmSwitch'},\n    const {'1': 'ai_switch', '3': 2, '4': 1, '5': 8, '10': 'aiSwitch'},\n    const {'1': 'ai_level', '3': 3, '4': 1, '5': 5, '10': 'aiLevel'},\n    const {'1': 'blocktop', '3': 4, '4': 1, '5': 8, '10': 'blocktop'},\n    const {'1': 'blockscroll', '3': 5, '4': 1, '5': 8, '10': 'blockscroll'},\n    const {'1': 'blockbottom', '3': 6, '4': 1, '5': 8, '10': 'blockbottom'},\n    const {'1': 'blockcolor', '3': 7, '4': 1, '5': 8, '10': 'blockcolor'},\n    const {'1': 'blockspecial', '3': 8, '4': 1, '5': 8, '10': 'blockspecial'},\n    const {'1': 'preventshade', '3': 9, '4': 1, '5': 8, '10': 'preventshade'},\n    const {'1': 'dmask', '3': 10, '4': 1, '5': 8, '10': 'dmask'},\n    const {'1': 'opacity', '3': 11, '4': 1, '5': 2, '10': 'opacity'},\n    const {'1': 'dmarea', '3': 12, '4': 1, '5': 5, '10': 'dmarea'},\n    const {'1': 'speedplus', '3': 13, '4': 1, '5': 2, '10': 'speedplus'},\n    const {'1': 'fontsize', '3': 14, '4': 1, '5': 2, '10': 'fontsize'},\n    const {'1': 'screensync', '3': 15, '4': 1, '5': 8, '10': 'screensync'},\n    const {'1': 'speedsync', '3': 16, '4': 1, '5': 8, '10': 'speedsync'},\n    const {'1': 'fontfamily', '3': 17, '4': 1, '5': 9, '10': 'fontfamily'},\n    const {'1': 'bold', '3': 18, '4': 1, '5': 8, '10': 'bold'},\n    const {'1': 'fontborder', '3': 19, '4': 1, '5': 5, '10': 'fontborder'},\n    const {'1': 'draw_type', '3': 20, '4': 1, '5': 9, '10': 'drawType'},\n    const {\n      '1': 'senior_mode_switch',\n      '3': 21,\n      '4': 1,\n      '5': 5,\n      '10': 'seniorModeSwitch'\n    },\n    const {'1': 'ai_level_v2', '3': 22, '4': 1, '5': 5, '10': 'aiLevelV2'},\n    const {\n      '1': 'ai_level_v2_map',\n      '3': 23,\n      '4': 3,\n      '5': 11,\n      '6':\n          '.bilibili.community.service.dm.v1.DanmuWebPlayerConfig.AiLevelV2MapEntry',\n      '10': 'aiLevelV2Map'\n    },\n  ],\n  '3': const [DanmuWebPlayerConfig_AiLevelV2MapEntry$json],\n};\n\n@$core.Deprecated('Use danmuWebPlayerConfigDescriptor instead')\nconst DanmuWebPlayerConfig_AiLevelV2MapEntry$json = const {\n  '1': 'AiLevelV2MapEntry',\n  '2': const [\n    const {'1': 'key', '3': 1, '4': 1, '5': 5, '10': 'key'},\n    const {'1': 'value', '3': 2, '4': 1, '5': 5, '10': 'value'},\n  ],\n  '7': const {'7': true},\n};\n\n/// Descriptor for `DanmuWebPlayerConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List danmuWebPlayerConfigDescriptor = $convert.base64Decode(\n    'ChREYW5tdVdlYlBsYXllckNvbmZpZxIbCglkbV9zd2l0Y2gYASABKAhSCGRtU3dpdGNoEhsKCWFpX3N3aXRjaBgCIAEoCFIIYWlTd2l0Y2gSGQoIYWlfbGV2ZWwYAyABKAVSB2FpTGV2ZWwSGgoIYmxvY2t0b3AYBCABKAhSCGJsb2NrdG9wEiAKC2Jsb2Nrc2Nyb2xsGAUgASgIUgtibG9ja3Njcm9sbBIgCgtibG9ja2JvdHRvbRgGIAEoCFILYmxvY2tib3R0b20SHgoKYmxvY2tjb2xvchgHIAEoCFIKYmxvY2tjb2xvchIiCgxibG9ja3NwZWNpYWwYCCABKAhSDGJsb2Nrc3BlY2lhbBIiCgxwcmV2ZW50c2hhZGUYCSABKAhSDHByZXZlbnRzaGFkZRIUCgVkbWFzaxgKIAEoCFIFZG1hc2sSGAoHb3BhY2l0eRgLIAEoAlIHb3BhY2l0eRIWCgZkbWFyZWEYDCABKAVSBmRtYXJlYRIcCglzcGVlZHBsdXMYDSABKAJSCXNwZWVkcGx1cxIaCghmb250c2l6ZRgOIAEoAlIIZm9udHNpemUSHgoKc2NyZWVuc3luYxgPIAEoCFIKc2NyZWVuc3luYxIcCglzcGVlZHN5bmMYECABKAhSCXNwZWVkc3luYxIeCgpmb250ZmFtaWx5GBEgASgJUgpmb250ZmFtaWx5EhIKBGJvbGQYEiABKAhSBGJvbGQSHgoKZm9udGJvcmRlchgTIAEoBVIKZm9udGJvcmRlchIbCglkcmF3X3R5cGUYFCABKAlSCGRyYXdUeXBlEiwKEnNlbmlvcl9tb2RlX3N3aXRjaBgVIAEoBVIQc2VuaW9yTW9kZVN3aXRjaBIeCgthaV9sZXZlbF92MhgWIAEoBVIJYWlMZXZlbFYyEm8KD2FpX2xldmVsX3YyX21hcBgXIAMoCzJILmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRhbm11V2ViUGxheWVyQ29uZmlnLkFpTGV2ZWxWMk1hcEVudHJ5UgxhaUxldmVsVjJNYXAaPwoRQWlMZXZlbFYyTWFwRW50cnkSEAoDa2V5GAEgASgFUgNrZXkSFAoFdmFsdWUYAiABKAVSBXZhbHVlOgI4AQ==');\n@$core.Deprecated('Use dmExpoReportReqDescriptor instead')\nconst DmExpoReportReq$json = const {\n  '1': 'DmExpoReportReq',\n  '2': const [\n    const {'1': 'session_id', '3': 1, '4': 1, '5': 9, '10': 'sessionId'},\n    const {'1': 'oid', '3': 2, '4': 1, '5': 3, '10': 'oid'},\n    const {'1': 'spmid', '3': 4, '4': 1, '5': 9, '10': 'spmid'},\n  ],\n};\n\n/// Descriptor for `DmExpoReportReq`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmExpoReportReqDescriptor = $convert.base64Decode(\n    'Cg9EbUV4cG9SZXBvcnRSZXESHQoKc2Vzc2lvbl9pZBgBIAEoCVIJc2Vzc2lvbklkEhAKA29pZBgCIAEoA1IDb2lkEhQKBXNwbWlkGAQgASgJUgVzcG1pZA==');\n@$core.Deprecated('Use dmExpoReportResDescriptor instead')\nconst DmExpoReportRes$json = const {\n  '1': 'DmExpoReportRes',\n};\n\n/// Descriptor for `DmExpoReportRes`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmExpoReportResDescriptor =\n    $convert.base64Decode('Cg9EbUV4cG9SZXBvcnRSZXM=');\n@$core.Deprecated('Use dmPlayerConfigReqDescriptor instead')\nconst DmPlayerConfigReq$json = const {\n  '1': 'DmPlayerConfigReq',\n  '2': const [\n    const {'1': 'ts', '3': 1, '4': 1, '5': 3, '10': 'ts'},\n    const {\n      '1': 'switch',\n      '3': 2,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuSwitch',\n      '10': 'switch'\n    },\n    const {\n      '1': 'switch_save',\n      '3': 3,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuSwitchSave',\n      '10': 'switchSave'\n    },\n    const {\n      '1': 'use_default_config',\n      '3': 4,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuUseDefaultConfig',\n      '10': 'useDefaultConfig'\n    },\n    const {\n      '1': 'ai_recommended_switch',\n      '3': 5,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedSwitch',\n      '10': 'aiRecommendedSwitch'\n    },\n    const {\n      '1': 'ai_recommended_level',\n      '3': 6,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedLevel',\n      '10': 'aiRecommendedLevel'\n    },\n    const {\n      '1': 'blocktop',\n      '3': 7,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuBlocktop',\n      '10': 'blocktop'\n    },\n    const {\n      '1': 'blockscroll',\n      '3': 8,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuBlockscroll',\n      '10': 'blockscroll'\n    },\n    const {\n      '1': 'blockbottom',\n      '3': 9,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuBlockbottom',\n      '10': 'blockbottom'\n    },\n    const {\n      '1': 'blockcolorful',\n      '3': 10,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuBlockcolorful',\n      '10': 'blockcolorful'\n    },\n    const {\n      '1': 'blockrepeat',\n      '3': 11,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuBlockrepeat',\n      '10': 'blockrepeat'\n    },\n    const {\n      '1': 'blockspecial',\n      '3': 12,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuBlockspecial',\n      '10': 'blockspecial'\n    },\n    const {\n      '1': 'opacity',\n      '3': 13,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuOpacity',\n      '10': 'opacity'\n    },\n    const {\n      '1': 'scalingfactor',\n      '3': 14,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuScalingfactor',\n      '10': 'scalingfactor'\n    },\n    const {\n      '1': 'domain',\n      '3': 15,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuDomain',\n      '10': 'domain'\n    },\n    const {\n      '1': 'speed',\n      '3': 16,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuSpeed',\n      '10': 'speed'\n    },\n    const {\n      '1': 'enableblocklist',\n      '3': 17,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuEnableblocklist',\n      '10': 'enableblocklist'\n    },\n    const {\n      '1': 'inlinePlayerDanmakuSwitch',\n      '3': 18,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.InlinePlayerDanmakuSwitch',\n      '10': 'inlinePlayerDanmakuSwitch'\n    },\n    const {\n      '1': 'senior_mode_switch',\n      '3': 19,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PlayerDanmakuSeniorModeSwitch',\n      '10': 'seniorModeSwitch'\n    },\n    const {\n      '1': 'ai_recommended_level_v2',\n      '3': 20,\n      '4': 1,\n      '5': 11,\n      '6':\n          '.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedLevelV2',\n      '10': 'aiRecommendedLevelV2'\n    },\n  ],\n};\n\n/// Descriptor for `DmPlayerConfigReq`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmPlayerConfigReqDescriptor = $convert.base64Decode(\n    'ChFEbVBsYXllckNvbmZpZ1JlcRIOCgJ0cxgBIAEoA1ICdHMSTQoGc3dpdGNoGAIgASgLMjUuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUGxheWVyRGFubWFrdVN3aXRjaFIGc3dpdGNoEloKC3N3aXRjaF9zYXZlGAMgASgLMjkuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUGxheWVyRGFubWFrdVN3aXRjaFNhdmVSCnN3aXRjaFNhdmUSbQoSdXNlX2RlZmF1bHRfY29uZmlnGAQgASgLMj8uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUGxheWVyRGFubWFrdVVzZURlZmF1bHRDb25maWdSEHVzZURlZmF1bHRDb25maWcSdgoVYWlfcmVjb21tZW5kZWRfc3dpdGNoGAUgASgLMkIuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUGxheWVyRGFubWFrdUFpUmVjb21tZW5kZWRTd2l0Y2hSE2FpUmVjb21tZW5kZWRTd2l0Y2gScwoUYWlfcmVjb21tZW5kZWRfbGV2ZWwYBiABKAsyQS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5QbGF5ZXJEYW5tYWt1QWlSZWNvbW1lbmRlZExldmVsUhJhaVJlY29tbWVuZGVkTGV2ZWwSUwoIYmxvY2t0b3AYByABKAsyNy5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5QbGF5ZXJEYW5tYWt1QmxvY2t0b3BSCGJsb2NrdG9wElwKC2Jsb2Nrc2Nyb2xsGAggASgLMjouYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUGxheWVyRGFubWFrdUJsb2Nrc2Nyb2xsUgtibG9ja3Njcm9sbBJcCgtibG9ja2JvdHRvbRgJIAEoCzI6LmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlBsYXllckRhbm1ha3VCbG9ja2JvdHRvbVILYmxvY2tib3R0b20SYgoNYmxvY2tjb2xvcmZ1bBgKIAEoCzI8LmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlBsYXllckRhbm1ha3VCbG9ja2NvbG9yZnVsUg1ibG9ja2NvbG9yZnVsElwKC2Jsb2NrcmVwZWF0GAsgASgLMjouYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUGxheWVyRGFubWFrdUJsb2NrcmVwZWF0UgtibG9ja3JlcGVhdBJfCgxibG9ja3NwZWNpYWwYDCABKAsyOy5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5QbGF5ZXJEYW5tYWt1QmxvY2tzcGVjaWFsUgxibG9ja3NwZWNpYWwSUAoHb3BhY2l0eRgNIAEoCzI2LmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlBsYXllckRhbm1ha3VPcGFjaXR5UgdvcGFjaXR5EmIKDXNjYWxpbmdmYWN0b3IYDiABKAsyPC5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5QbGF5ZXJEYW5tYWt1U2NhbGluZ2ZhY3RvclINc2NhbGluZ2ZhY3RvchJNCgZkb21haW4YDyABKAsyNS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5QbGF5ZXJEYW5tYWt1RG9tYWluUgZkb21haW4SSgoFc3BlZWQYECABKAsyNC5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5QbGF5ZXJEYW5tYWt1U3BlZWRSBXNwZWVkEmgKD2VuYWJsZWJsb2NrbGlzdBgRIAEoCzI+LmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlBsYXllckRhbm1ha3VFbmFibGVibG9ja2xpc3RSD2VuYWJsZWJsb2NrbGlzdBJ5ChlpbmxpbmVQbGF5ZXJEYW5tYWt1U3dpdGNoGBIgASgLMjsuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuSW5saW5lUGxheWVyRGFubWFrdVN3aXRjaFIZaW5saW5lUGxheWVyRGFubWFrdVN3aXRjaBJtChJzZW5pb3JfbW9kZV9zd2l0Y2gYEyABKAsyPy5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5QbGF5ZXJEYW5tYWt1U2VuaW9yTW9kZVN3aXRjaFIQc2VuaW9yTW9kZVN3aXRjaBJ6ChdhaV9yZWNvbW1lbmRlZF9sZXZlbF92MhgUIAEoCzJDLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlBsYXllckRhbm1ha3VBaVJlY29tbWVuZGVkTGV2ZWxWMlIUYWlSZWNvbW1lbmRlZExldmVsVjI=');\n@$core.Deprecated('Use dmSegConfigDescriptor instead')\nconst DmSegConfig$json = const {\n  '1': 'DmSegConfig',\n  '2': const [\n    const {'1': 'page_size', '3': 1, '4': 1, '5': 3, '10': 'pageSize'},\n    const {'1': 'total', '3': 2, '4': 1, '5': 3, '10': 'total'},\n  ],\n};\n\n/// Descriptor for `DmSegConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmSegConfigDescriptor = $convert.base64Decode(\n    'CgtEbVNlZ0NvbmZpZxIbCglwYWdlX3NpemUYASABKANSCHBhZ2VTaXplEhQKBXRvdGFsGAIgASgDUgV0b3RhbA==');\n@$core.Deprecated('Use dmSegMobileReplyDescriptor instead')\nconst DmSegMobileReply$json = const {\n  '1': 'DmSegMobileReply',\n  '2': const [\n    const {\n      '1': 'elems',\n      '3': 1,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmakuElem',\n      '10': 'elems'\n    },\n    const {'1': 'state', '3': 2, '4': 1, '5': 5, '10': 'state'},\n    const {\n      '1': 'ai_flag',\n      '3': 3,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmakuAIFlag',\n      '10': 'aiFlag'\n    },\n  ],\n};\n\n/// Descriptor for `DmSegMobileReply`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmSegMobileReplyDescriptor = $convert.base64Decode(\n    'ChBEbVNlZ01vYmlsZVJlcGx5EkMKBWVsZW1zGAEgAygLMi0uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRGFubWFrdUVsZW1SBWVsZW1zEhQKBXN0YXRlGAIgASgFUgVzdGF0ZRJICgdhaV9mbGFnGAMgASgLMi8uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRGFubWFrdUFJRmxhZ1IGYWlGbGFn');\n@$core.Deprecated('Use dmSegMobileReqDescriptor instead')\nconst DmSegMobileReq$json = const {\n  '1': 'DmSegMobileReq',\n  '2': const [\n    const {'1': 'pid', '3': 1, '4': 1, '5': 3, '10': 'pid'},\n    const {'1': 'oid', '3': 2, '4': 1, '5': 3, '10': 'oid'},\n    const {'1': 'type', '3': 3, '4': 1, '5': 5, '10': 'type'},\n    const {'1': 'segment_index', '3': 4, '4': 1, '5': 3, '10': 'segmentIndex'},\n    const {\n      '1': 'teenagers_mode',\n      '3': 5,\n      '4': 1,\n      '5': 5,\n      '10': 'teenagersMode'\n    },\n    const {'1': 'ps', '3': 6, '4': 1, '5': 3, '10': 'ps'},\n    const {'1': 'pe', '3': 7, '4': 1, '5': 3, '10': 'pe'},\n    const {'1': 'pull_mode', '3': 8, '4': 1, '5': 5, '10': 'pullMode'},\n    const {'1': 'from_scene', '3': 9, '4': 1, '5': 5, '10': 'fromScene'},\n  ],\n};\n\n/// Descriptor for `DmSegMobileReq`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmSegMobileReqDescriptor = $convert.base64Decode(\n    'Cg5EbVNlZ01vYmlsZVJlcRIQCgNwaWQYASABKANSA3BpZBIQCgNvaWQYAiABKANSA29pZBISCgR0eXBlGAMgASgFUgR0eXBlEiMKDXNlZ21lbnRfaW5kZXgYBCABKANSDHNlZ21lbnRJbmRleBIlCg50ZWVuYWdlcnNfbW9kZRgFIAEoBVINdGVlbmFnZXJzTW9kZRIOCgJwcxgGIAEoA1ICcHMSDgoCcGUYByABKANSAnBlEhsKCXB1bGxfbW9kZRgIIAEoBVIIcHVsbE1vZGUSHQoKZnJvbV9zY2VuZRgJIAEoBVIJZnJvbVNjZW5l');\n@$core.Deprecated('Use dmSegOttReplyDescriptor instead')\nconst DmSegOttReply$json = const {\n  '1': 'DmSegOttReply',\n  '2': const [\n    const {'1': 'closed', '3': 1, '4': 1, '5': 8, '10': 'closed'},\n    const {\n      '1': 'elems',\n      '3': 2,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmakuElem',\n      '10': 'elems'\n    },\n  ],\n};\n\n/// Descriptor for `DmSegOttReply`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmSegOttReplyDescriptor = $convert.base64Decode(\n    'Cg1EbVNlZ090dFJlcGx5EhYKBmNsb3NlZBgBIAEoCFIGY2xvc2VkEkMKBWVsZW1zGAIgAygLMi0uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRGFubWFrdUVsZW1SBWVsZW1z');\n@$core.Deprecated('Use dmSegOttReqDescriptor instead')\nconst DmSegOttReq$json = const {\n  '1': 'DmSegOttReq',\n  '2': const [\n    const {'1': 'pid', '3': 1, '4': 1, '5': 3, '10': 'pid'},\n    const {'1': 'oid', '3': 2, '4': 1, '5': 3, '10': 'oid'},\n    const {'1': 'type', '3': 3, '4': 1, '5': 5, '10': 'type'},\n    const {'1': 'segment_index', '3': 4, '4': 1, '5': 3, '10': 'segmentIndex'},\n  ],\n};\n\n/// Descriptor for `DmSegOttReq`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmSegOttReqDescriptor = $convert.base64Decode(\n    'CgtEbVNlZ090dFJlcRIQCgNwaWQYASABKANSA3BpZBIQCgNvaWQYAiABKANSA29pZBISCgR0eXBlGAMgASgFUgR0eXBlEiMKDXNlZ21lbnRfaW5kZXgYBCABKANSDHNlZ21lbnRJbmRleA==');\n@$core.Deprecated('Use dmSegSDKReplyDescriptor instead')\nconst DmSegSDKReply$json = const {\n  '1': 'DmSegSDKReply',\n  '2': const [\n    const {'1': 'closed', '3': 1, '4': 1, '5': 8, '10': 'closed'},\n    const {\n      '1': 'elems',\n      '3': 2,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmakuElem',\n      '10': 'elems'\n    },\n  ],\n};\n\n/// Descriptor for `DmSegSDKReply`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmSegSDKReplyDescriptor = $convert.base64Decode(\n    'Cg1EbVNlZ1NES1JlcGx5EhYKBmNsb3NlZBgBIAEoCFIGY2xvc2VkEkMKBWVsZW1zGAIgAygLMi0uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRGFubWFrdUVsZW1SBWVsZW1z');\n@$core.Deprecated('Use dmSegSDKReqDescriptor instead')\nconst DmSegSDKReq$json = const {\n  '1': 'DmSegSDKReq',\n  '2': const [\n    const {'1': 'pid', '3': 1, '4': 1, '5': 3, '10': 'pid'},\n    const {'1': 'oid', '3': 2, '4': 1, '5': 3, '10': 'oid'},\n    const {'1': 'type', '3': 3, '4': 1, '5': 5, '10': 'type'},\n    const {'1': 'segment_index', '3': 4, '4': 1, '5': 3, '10': 'segmentIndex'},\n  ],\n};\n\n/// Descriptor for `DmSegSDKReq`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmSegSDKReqDescriptor = $convert.base64Decode(\n    'CgtEbVNlZ1NES1JlcRIQCgNwaWQYASABKANSA3BpZBIQCgNvaWQYAiABKANSA29pZBISCgR0eXBlGAMgASgFUgR0eXBlEiMKDXNlZ21lbnRfaW5kZXgYBCABKANSDHNlZ21lbnRJbmRleA==');\n@$core.Deprecated('Use dmViewReplyDescriptor instead')\nconst DmViewReply$json = const {\n  '1': 'DmViewReply',\n  '2': const [\n    const {'1': 'closed', '3': 1, '4': 1, '5': 8, '10': 'closed'},\n    const {\n      '1': 'mask',\n      '3': 2,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.VideoMask',\n      '10': 'mask'\n    },\n    const {\n      '1': 'subtitle',\n      '3': 3,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.VideoSubtitle',\n      '10': 'subtitle'\n    },\n    const {'1': 'special_dms', '3': 4, '4': 3, '5': 9, '10': 'specialDms'},\n    const {\n      '1': 'ai_flag',\n      '3': 5,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmakuFlagConfig',\n      '10': 'aiFlag'\n    },\n    const {\n      '1': 'player_config',\n      '3': 6,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmuPlayerViewConfig',\n      '10': 'playerConfig'\n    },\n    const {'1': 'send_box_style', '3': 7, '4': 1, '5': 5, '10': 'sendBoxStyle'},\n    const {'1': 'allow', '3': 8, '4': 1, '5': 8, '10': 'allow'},\n    const {'1': 'check_box', '3': 9, '4': 1, '5': 9, '10': 'checkBox'},\n    const {\n      '1': 'check_box_show_msg',\n      '3': 10,\n      '4': 1,\n      '5': 9,\n      '10': 'checkBoxShowMsg'\n    },\n    const {\n      '1': 'text_placeholder',\n      '3': 11,\n      '4': 1,\n      '5': 9,\n      '10': 'textPlaceholder'\n    },\n    const {\n      '1': 'input_placeholder',\n      '3': 12,\n      '4': 1,\n      '5': 9,\n      '10': 'inputPlaceholder'\n    },\n    const {\n      '1': 'report_filter_content',\n      '3': 13,\n      '4': 3,\n      '5': 9,\n      '10': 'reportFilterContent'\n    },\n    const {\n      '1': 'expo_report',\n      '3': 14,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.ExpoReport',\n      '10': 'expoReport'\n    },\n    const {\n      '1': 'buzzword_config',\n      '3': 15,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.BuzzwordConfig',\n      '10': 'buzzwordConfig'\n    },\n    const {\n      '1': 'expressions',\n      '3': 16,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Expressions',\n      '10': 'expressions'\n    },\n    const {\n      '1': 'post_panel',\n      '3': 17,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PostPanel',\n      '10': 'postPanel'\n    },\n    const {'1': 'activity_meta', '3': 18, '4': 3, '5': 9, '10': 'activityMeta'},\n    const {\n      '1': 'post_panel2',\n      '3': 19,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PostPanelV2',\n      '10': 'postPanel2'\n    },\n  ],\n};\n\n/// Descriptor for `DmViewReply`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmViewReplyDescriptor = $convert.base64Decode(\n    'CgtEbVZpZXdSZXBseRIWCgZjbG9zZWQYASABKAhSBmNsb3NlZBI/CgRtYXNrGAIgASgLMisuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuVmlkZW9NYXNrUgRtYXNrEksKCHN1YnRpdGxlGAMgASgLMi8uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuVmlkZW9TdWJ0aXRsZVIIc3VidGl0bGUSHwoLc3BlY2lhbF9kbXMYBCADKAlSCnNwZWNpYWxEbXMSTAoHYWlfZmxhZxgFIAEoCzIzLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRhbm1ha3VGbGFnQ29uZmlnUgZhaUZsYWcSXAoNcGxheWVyX2NvbmZpZxgGIAEoCzI3LmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRhbm11UGxheWVyVmlld0NvbmZpZ1IMcGxheWVyQ29uZmlnEiQKDnNlbmRfYm94X3N0eWxlGAcgASgFUgxzZW5kQm94U3R5bGUSFAoFYWxsb3cYCCABKAhSBWFsbG93EhsKCWNoZWNrX2JveBgJIAEoCVIIY2hlY2tCb3gSKwoSY2hlY2tfYm94X3Nob3dfbXNnGAogASgJUg9jaGVja0JveFNob3dNc2cSKQoQdGV4dF9wbGFjZWhvbGRlchgLIAEoCVIPdGV4dFBsYWNlaG9sZGVyEisKEWlucHV0X3BsYWNlaG9sZGVyGAwgASgJUhBpbnB1dFBsYWNlaG9sZGVyEjIKFXJlcG9ydF9maWx0ZXJfY29udGVudBgNIAMoCVITcmVwb3J0RmlsdGVyQ29udGVudBJNCgtleHBvX3JlcG9ydBgOIAEoCzIsLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkV4cG9SZXBvcnRSCmV4cG9SZXBvcnQSWQoPYnV6endvcmRfY29uZmlnGA8gASgLMjAuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuQnV6endvcmRDb25maWdSDmJ1enp3b3JkQ29uZmlnEk8KC2V4cHJlc3Npb25zGBAgAygLMi0uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRXhwcmVzc2lvbnNSC2V4cHJlc3Npb25zEkoKCnBvc3RfcGFuZWwYESADKAsyKy5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5Qb3N0UGFuZWxSCXBvc3RQYW5lbBIjCg1hY3Rpdml0eV9tZXRhGBIgAygJUgxhY3Rpdml0eU1ldGESTgoLcG9zdF9wYW5lbDIYEyADKAsyLS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5Qb3N0UGFuZWxWMlIKcG9zdFBhbmVsMg==');\n@$core.Deprecated('Use dmViewReqDescriptor instead')\nconst DmViewReq$json = const {\n  '1': 'DmViewReq',\n  '2': const [\n    const {'1': 'pid', '3': 1, '4': 1, '5': 3, '10': 'pid'},\n    const {'1': 'oid', '3': 2, '4': 1, '5': 3, '10': 'oid'},\n    const {'1': 'type', '3': 3, '4': 1, '5': 5, '10': 'type'},\n    const {'1': 'spmid', '3': 4, '4': 1, '5': 9, '10': 'spmid'},\n    const {'1': 'is_hard_boot', '3': 5, '4': 1, '5': 5, '10': 'isHardBoot'},\n  ],\n};\n\n/// Descriptor for `DmViewReq`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmViewReqDescriptor = $convert.base64Decode(\n    'CglEbVZpZXdSZXESEAoDcGlkGAEgASgDUgNwaWQSEAoDb2lkGAIgASgDUgNvaWQSEgoEdHlwZRgDIAEoBVIEdHlwZRIUCgVzcG1pZBgEIAEoCVIFc3BtaWQSIAoMaXNfaGFyZF9ib290GAUgASgFUgppc0hhcmRCb290');\n@$core.Deprecated('Use dmWebViewReplyDescriptor instead')\nconst DmWebViewReply$json = const {\n  '1': 'DmWebViewReply',\n  '2': const [\n    const {'1': 'state', '3': 1, '4': 1, '5': 5, '10': 'state'},\n    const {'1': 'text', '3': 2, '4': 1, '5': 9, '10': 'text'},\n    const {'1': 'text_side', '3': 3, '4': 1, '5': 9, '10': 'textSide'},\n    const {\n      '1': 'dm_sge',\n      '3': 4,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DmSegConfig',\n      '10': 'dmSge'\n    },\n    const {\n      '1': 'flag',\n      '3': 5,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmakuFlagConfig',\n      '10': 'flag'\n    },\n    const {'1': 'special_dms', '3': 6, '4': 3, '5': 9, '10': 'specialDms'},\n    const {'1': 'check_box', '3': 7, '4': 1, '5': 8, '10': 'checkBox'},\n    const {'1': 'count', '3': 8, '4': 1, '5': 3, '10': 'count'},\n    const {\n      '1': 'commandDms',\n      '3': 9,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.CommandDm',\n      '10': 'commandDms'\n    },\n    const {\n      '1': 'player_config',\n      '3': 10,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.DanmuWebPlayerConfig',\n      '10': 'playerConfig'\n    },\n    const {\n      '1': 'report_filter_content',\n      '3': 11,\n      '4': 3,\n      '5': 9,\n      '10': 'reportFilterContent'\n    },\n    const {\n      '1': 'expressions',\n      '3': 12,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Expressions',\n      '10': 'expressions'\n    },\n    const {\n      '1': 'post_panel',\n      '3': 13,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.PostPanel',\n      '10': 'postPanel'\n    },\n    const {'1': 'activity_meta', '3': 14, '4': 3, '5': 9, '10': 'activityMeta'},\n  ],\n};\n\n/// Descriptor for `DmWebViewReply`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List dmWebViewReplyDescriptor = $convert.base64Decode(\n    'Cg5EbVdlYlZpZXdSZXBseRIUCgVzdGF0ZRgBIAEoBVIFc3RhdGUSEgoEdGV4dBgCIAEoCVIEdGV4dBIbCgl0ZXh0X3NpZGUYAyABKAlSCHRleHRTaWRlEkQKBmRtX3NnZRgEIAEoCzItLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRtU2VnQ29uZmlnUgVkbVNnZRJHCgRmbGFnGAUgASgLMjMuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRGFubWFrdUZsYWdDb25maWdSBGZsYWcSHwoLc3BlY2lhbF9kbXMYBiADKAlSCnNwZWNpYWxEbXMSGwoJY2hlY2tfYm94GAcgASgIUghjaGVja0JveBIUCgVjb3VudBgIIAEoA1IFY291bnQSSwoKY29tbWFuZERtcxgJIAMoCzIrLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkNvbW1hbmREbVIKY29tbWFuZERtcxJbCg1wbGF5ZXJfY29uZmlnGAogASgLMjYuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRGFubXVXZWJQbGF5ZXJDb25maWdSDHBsYXllckNvbmZpZxIyChVyZXBvcnRfZmlsdGVyX2NvbnRlbnQYCyADKAlSE3JlcG9ydEZpbHRlckNvbnRlbnQSTwoLZXhwcmVzc2lvbnMYDCADKAsyLS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5FeHByZXNzaW9uc1ILZXhwcmVzc2lvbnMSSgoKcG9zdF9wYW5lbBgNIAMoCzIrLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlBvc3RQYW5lbFIJcG9zdFBhbmVsEiMKDWFjdGl2aXR5X21ldGEYDiADKAlSDGFjdGl2aXR5TWV0YQ==');\n@$core.Deprecated('Use expoReportDescriptor instead')\nconst ExpoReport$json = const {\n  '1': 'ExpoReport',\n  '2': const [\n    const {\n      '1': 'should_report_at_end',\n      '3': 1,\n      '4': 1,\n      '5': 8,\n      '10': 'shouldReportAtEnd'\n    },\n  ],\n};\n\n/// Descriptor for `ExpoReport`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List expoReportDescriptor = $convert.base64Decode(\n    'CgpFeHBvUmVwb3J0Ei8KFHNob3VsZF9yZXBvcnRfYXRfZW5kGAEgASgIUhFzaG91bGRSZXBvcnRBdEVuZA==');\n@$core.Deprecated('Use expressionDescriptor instead')\nconst Expression$json = const {\n  '1': 'Expression',\n  '2': const [\n    const {'1': 'keyword', '3': 1, '4': 3, '5': 9, '10': 'keyword'},\n    const {'1': 'url', '3': 2, '4': 1, '5': 9, '10': 'url'},\n    const {\n      '1': 'period',\n      '3': 3,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Period',\n      '10': 'period'\n    },\n  ],\n};\n\n/// Descriptor for `Expression`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List expressionDescriptor = $convert.base64Decode(\n    'CgpFeHByZXNzaW9uEhgKB2tleXdvcmQYASADKAlSB2tleXdvcmQSEAoDdXJsGAIgASgJUgN1cmwSQAoGcGVyaW9kGAMgAygLMiguYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUGVyaW9kUgZwZXJpb2Q=');\n@$core.Deprecated('Use expressionsDescriptor instead')\nconst Expressions$json = const {\n  '1': 'Expressions',\n  '2': const [\n    const {\n      '1': 'data',\n      '3': 1,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Expression',\n      '10': 'data'\n    },\n  ],\n};\n\n/// Descriptor for `Expressions`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List expressionsDescriptor = $convert.base64Decode(\n    'CgtFeHByZXNzaW9ucxJACgRkYXRhGAEgAygLMiwuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRXhwcmVzc2lvblIEZGF0YQ==');\n@$core.Deprecated('Use inlinePlayerDanmakuSwitchDescriptor instead')\nconst InlinePlayerDanmakuSwitch$json = const {\n  '1': 'InlinePlayerDanmakuSwitch',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `InlinePlayerDanmakuSwitch`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List inlinePlayerDanmakuSwitchDescriptor =\n    $convert.base64Decode(\n        'ChlJbmxpbmVQbGF5ZXJEYW5tYWt1U3dpdGNoEhQKBXZhbHVlGAEgASgIUgV2YWx1ZQ==');\n@$core.Deprecated('Use labelDescriptor instead')\nconst Label$json = const {\n  '1': 'Label',\n  '2': const [\n    const {'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'},\n    const {'1': 'content', '3': 2, '4': 3, '5': 9, '10': 'content'},\n  ],\n};\n\n/// Descriptor for `Label`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List labelDescriptor = $convert.base64Decode(\n    'CgVMYWJlbBIUCgV0aXRsZRgBIAEoCVIFdGl0bGUSGAoHY29udGVudBgCIAMoCVIHY29udGVudA==');\n@$core.Deprecated('Use labelV2Descriptor instead')\nconst LabelV2$json = const {\n  '1': 'LabelV2',\n  '2': const [\n    const {'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'},\n    const {'1': 'content', '3': 2, '4': 3, '5': 9, '10': 'content'},\n    const {'1': 'exposure_once', '3': 3, '4': 1, '5': 8, '10': 'exposureOnce'},\n    const {'1': 'exposure_type', '3': 4, '4': 1, '5': 5, '10': 'exposureType'},\n  ],\n};\n\n/// Descriptor for `LabelV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List labelV2Descriptor = $convert.base64Decode(\n    'CgdMYWJlbFYyEhQKBXRpdGxlGAEgASgJUgV0aXRsZRIYCgdjb250ZW50GAIgAygJUgdjb250ZW50EiMKDWV4cG9zdXJlX29uY2UYAyABKAhSDGV4cG9zdXJlT25jZRIjCg1leHBvc3VyZV90eXBlGAQgASgFUgxleHBvc3VyZVR5cGU=');\n@$core.Deprecated('Use periodDescriptor instead')\nconst Period$json = const {\n  '1': 'Period',\n  '2': const [\n    const {'1': 'start', '3': 1, '4': 1, '5': 3, '10': 'start'},\n    const {'1': 'end', '3': 2, '4': 1, '5': 3, '10': 'end'},\n  ],\n};\n\n/// Descriptor for `Period`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List periodDescriptor = $convert.base64Decode(\n    'CgZQZXJpb2QSFAoFc3RhcnQYASABKANSBXN0YXJ0EhAKA2VuZBgCIAEoA1IDZW5k');\n@$core.Deprecated('Use playerDanmakuAiRecommendedLevelDescriptor instead')\nconst PlayerDanmakuAiRecommendedLevel$json = const {\n  '1': 'PlayerDanmakuAiRecommendedLevel',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuAiRecommendedLevel`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuAiRecommendedLevelDescriptor =\n    $convert.base64Decode(\n        'Ch9QbGF5ZXJEYW5tYWt1QWlSZWNvbW1lbmRlZExldmVsEhQKBXZhbHVlGAEgASgIUgV2YWx1ZQ==');\n@$core.Deprecated('Use playerDanmakuAiRecommendedLevelV2Descriptor instead')\nconst PlayerDanmakuAiRecommendedLevelV2$json = const {\n  '1': 'PlayerDanmakuAiRecommendedLevelV2',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 5, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuAiRecommendedLevelV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuAiRecommendedLevelV2Descriptor =\n    $convert.base64Decode(\n        'CiFQbGF5ZXJEYW5tYWt1QWlSZWNvbW1lbmRlZExldmVsVjISFAoFdmFsdWUYASABKAVSBXZhbHVl');\n@$core.Deprecated('Use playerDanmakuAiRecommendedSwitchDescriptor instead')\nconst PlayerDanmakuAiRecommendedSwitch$json = const {\n  '1': 'PlayerDanmakuAiRecommendedSwitch',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuAiRecommendedSwitch`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuAiRecommendedSwitchDescriptor =\n    $convert.base64Decode(\n        'CiBQbGF5ZXJEYW5tYWt1QWlSZWNvbW1lbmRlZFN3aXRjaBIUCgV2YWx1ZRgBIAEoCFIFdmFsdWU=');\n@$core.Deprecated('Use playerDanmakuBlockbottomDescriptor instead')\nconst PlayerDanmakuBlockbottom$json = const {\n  '1': 'PlayerDanmakuBlockbottom',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuBlockbottom`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuBlockbottomDescriptor =\n    $convert.base64Decode(\n        'ChhQbGF5ZXJEYW5tYWt1QmxvY2tib3R0b20SFAoFdmFsdWUYASABKAhSBXZhbHVl');\n@$core.Deprecated('Use playerDanmakuBlockcolorfulDescriptor instead')\nconst PlayerDanmakuBlockcolorful$json = const {\n  '1': 'PlayerDanmakuBlockcolorful',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuBlockcolorful`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuBlockcolorfulDescriptor =\n    $convert.base64Decode(\n        'ChpQbGF5ZXJEYW5tYWt1QmxvY2tjb2xvcmZ1bBIUCgV2YWx1ZRgBIAEoCFIFdmFsdWU=');\n@$core.Deprecated('Use playerDanmakuBlockrepeatDescriptor instead')\nconst PlayerDanmakuBlockrepeat$json = const {\n  '1': 'PlayerDanmakuBlockrepeat',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuBlockrepeat`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuBlockrepeatDescriptor =\n    $convert.base64Decode(\n        'ChhQbGF5ZXJEYW5tYWt1QmxvY2tyZXBlYXQSFAoFdmFsdWUYASABKAhSBXZhbHVl');\n@$core.Deprecated('Use playerDanmakuBlockscrollDescriptor instead')\nconst PlayerDanmakuBlockscroll$json = const {\n  '1': 'PlayerDanmakuBlockscroll',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuBlockscroll`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuBlockscrollDescriptor =\n    $convert.base64Decode(\n        'ChhQbGF5ZXJEYW5tYWt1QmxvY2tzY3JvbGwSFAoFdmFsdWUYASABKAhSBXZhbHVl');\n@$core.Deprecated('Use playerDanmakuBlockspecialDescriptor instead')\nconst PlayerDanmakuBlockspecial$json = const {\n  '1': 'PlayerDanmakuBlockspecial',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuBlockspecial`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuBlockspecialDescriptor =\n    $convert.base64Decode(\n        'ChlQbGF5ZXJEYW5tYWt1QmxvY2tzcGVjaWFsEhQKBXZhbHVlGAEgASgIUgV2YWx1ZQ==');\n@$core.Deprecated('Use playerDanmakuBlocktopDescriptor instead')\nconst PlayerDanmakuBlocktop$json = const {\n  '1': 'PlayerDanmakuBlocktop',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuBlocktop`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuBlocktopDescriptor =\n    $convert.base64Decode(\n        'ChVQbGF5ZXJEYW5tYWt1QmxvY2t0b3ASFAoFdmFsdWUYASABKAhSBXZhbHVl');\n@$core.Deprecated('Use playerDanmakuDomainDescriptor instead')\nconst PlayerDanmakuDomain$json = const {\n  '1': 'PlayerDanmakuDomain',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 2, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuDomain`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuDomainDescriptor =\n    $convert.base64Decode(\n        'ChNQbGF5ZXJEYW5tYWt1RG9tYWluEhQKBXZhbHVlGAEgASgCUgV2YWx1ZQ==');\n@$core.Deprecated('Use playerDanmakuEnableblocklistDescriptor instead')\nconst PlayerDanmakuEnableblocklist$json = const {\n  '1': 'PlayerDanmakuEnableblocklist',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuEnableblocklist`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuEnableblocklistDescriptor =\n    $convert.base64Decode(\n        'ChxQbGF5ZXJEYW5tYWt1RW5hYmxlYmxvY2tsaXN0EhQKBXZhbHVlGAEgASgIUgV2YWx1ZQ==');\n@$core.Deprecated('Use playerDanmakuOpacityDescriptor instead')\nconst PlayerDanmakuOpacity$json = const {\n  '1': 'PlayerDanmakuOpacity',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 2, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuOpacity`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuOpacityDescriptor =\n    $convert.base64Decode(\n        'ChRQbGF5ZXJEYW5tYWt1T3BhY2l0eRIUCgV2YWx1ZRgBIAEoAlIFdmFsdWU=');\n@$core.Deprecated('Use playerDanmakuScalingfactorDescriptor instead')\nconst PlayerDanmakuScalingfactor$json = const {\n  '1': 'PlayerDanmakuScalingfactor',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 2, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuScalingfactor`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuScalingfactorDescriptor =\n    $convert.base64Decode(\n        'ChpQbGF5ZXJEYW5tYWt1U2NhbGluZ2ZhY3RvchIUCgV2YWx1ZRgBIAEoAlIFdmFsdWU=');\n@$core.Deprecated('Use playerDanmakuSeniorModeSwitchDescriptor instead')\nconst PlayerDanmakuSeniorModeSwitch$json = const {\n  '1': 'PlayerDanmakuSeniorModeSwitch',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 5, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuSeniorModeSwitch`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuSeniorModeSwitchDescriptor =\n    $convert.base64Decode(\n        'Ch1QbGF5ZXJEYW5tYWt1U2VuaW9yTW9kZVN3aXRjaBIUCgV2YWx1ZRgBIAEoBVIFdmFsdWU=');\n@$core.Deprecated('Use playerDanmakuSpeedDescriptor instead')\nconst PlayerDanmakuSpeed$json = const {\n  '1': 'PlayerDanmakuSpeed',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 5, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuSpeed`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuSpeedDescriptor = $convert\n    .base64Decode('ChJQbGF5ZXJEYW5tYWt1U3BlZWQSFAoFdmFsdWUYASABKAVSBXZhbHVl');\n@$core.Deprecated('Use playerDanmakuSwitchDescriptor instead')\nconst PlayerDanmakuSwitch$json = const {\n  '1': 'PlayerDanmakuSwitch',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n    const {'1': 'can_ignore', '3': 2, '4': 1, '5': 8, '10': 'canIgnore'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuSwitch`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuSwitchDescriptor = $convert.base64Decode(\n    'ChNQbGF5ZXJEYW5tYWt1U3dpdGNoEhQKBXZhbHVlGAEgASgIUgV2YWx1ZRIdCgpjYW5faWdub3JlGAIgASgIUgljYW5JZ25vcmU=');\n@$core.Deprecated('Use playerDanmakuSwitchSaveDescriptor instead')\nconst PlayerDanmakuSwitchSave$json = const {\n  '1': 'PlayerDanmakuSwitchSave',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuSwitchSave`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuSwitchSaveDescriptor =\n    $convert.base64Decode(\n        'ChdQbGF5ZXJEYW5tYWt1U3dpdGNoU2F2ZRIUCgV2YWx1ZRgBIAEoCFIFdmFsdWU=');\n@$core.Deprecated('Use playerDanmakuUseDefaultConfigDescriptor instead')\nconst PlayerDanmakuUseDefaultConfig$json = const {\n  '1': 'PlayerDanmakuUseDefaultConfig',\n  '2': const [\n    const {'1': 'value', '3': 1, '4': 1, '5': 8, '10': 'value'},\n  ],\n};\n\n/// Descriptor for `PlayerDanmakuUseDefaultConfig`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List playerDanmakuUseDefaultConfigDescriptor =\n    $convert.base64Decode(\n        'Ch1QbGF5ZXJEYW5tYWt1VXNlRGVmYXVsdENvbmZpZxIUCgV2YWx1ZRgBIAEoCFIFdmFsdWU=');\n@$core.Deprecated('Use postPanelDescriptor instead')\nconst PostPanel$json = const {\n  '1': 'PostPanel',\n  '2': const [\n    const {'1': 'start', '3': 1, '4': 1, '5': 3, '10': 'start'},\n    const {'1': 'end', '3': 2, '4': 1, '5': 3, '10': 'end'},\n    const {'1': 'priority', '3': 3, '4': 1, '5': 3, '10': 'priority'},\n    const {'1': 'biz_id', '3': 4, '4': 1, '5': 3, '10': 'bizId'},\n    const {\n      '1': 'biz_type',\n      '3': 5,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.PostPanelBizType',\n      '10': 'bizType'\n    },\n    const {\n      '1': 'click_button',\n      '3': 6,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.ClickButton',\n      '10': 'clickButton'\n    },\n    const {\n      '1': 'text_input',\n      '3': 7,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.TextInput',\n      '10': 'textInput'\n    },\n    const {\n      '1': 'check_box',\n      '3': 8,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.CheckBox',\n      '10': 'checkBox'\n    },\n    const {\n      '1': 'toast',\n      '3': 9,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Toast',\n      '10': 'toast'\n    },\n  ],\n};\n\n/// Descriptor for `PostPanel`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List postPanelDescriptor = $convert.base64Decode(\n    'CglQb3N0UGFuZWwSFAoFc3RhcnQYASABKANSBXN0YXJ0EhAKA2VuZBgCIAEoA1IDZW5kEhoKCHByaW9yaXR5GAMgASgDUghwcmlvcml0eRIVCgZiaXpfaWQYBCABKANSBWJpeklkEk0KCGJpel90eXBlGAUgASgOMjIuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUG9zdFBhbmVsQml6VHlwZVIHYml6VHlwZRJQCgxjbGlja19idXR0b24YBiABKAsyLS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5DbGlja0J1dHRvblILY2xpY2tCdXR0b24SSgoKdGV4dF9pbnB1dBgHIAEoCzIrLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlRleHRJbnB1dFIJdGV4dElucHV0EkcKCWNoZWNrX2JveBgIIAEoCzIqLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkNoZWNrQm94UghjaGVja0JveBI9CgV0b2FzdBgJIAEoCzInLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlRvYXN0UgV0b2FzdA==');\n@$core.Deprecated('Use postPanelV2Descriptor instead')\nconst PostPanelV2$json = const {\n  '1': 'PostPanelV2',\n  '2': const [\n    const {'1': 'start', '3': 1, '4': 1, '5': 3, '10': 'start'},\n    const {'1': 'end', '3': 2, '4': 1, '5': 3, '10': 'end'},\n    const {'1': 'biz_type', '3': 3, '4': 1, '5': 5, '10': 'bizType'},\n    const {\n      '1': 'click_button',\n      '3': 4,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.ClickButtonV2',\n      '10': 'clickButton'\n    },\n    const {\n      '1': 'text_input',\n      '3': 5,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.TextInputV2',\n      '10': 'textInput'\n    },\n    const {\n      '1': 'check_box',\n      '3': 6,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.CheckBoxV2',\n      '10': 'checkBox'\n    },\n    const {\n      '1': 'toast',\n      '3': 7,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.ToastV2',\n      '10': 'toast'\n    },\n    const {\n      '1': 'bubble',\n      '3': 8,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.BubbleV2',\n      '10': 'bubble'\n    },\n    const {\n      '1': 'label',\n      '3': 9,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.LabelV2',\n      '10': 'label'\n    },\n    const {'1': 'post_status', '3': 10, '4': 1, '5': 5, '10': 'postStatus'},\n  ],\n};\n\n/// Descriptor for `PostPanelV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List postPanelV2Descriptor = $convert.base64Decode(\n    'CgtQb3N0UGFuZWxWMhIUCgVzdGFydBgBIAEoA1IFc3RhcnQSEAoDZW5kGAIgASgDUgNlbmQSGQoIYml6X3R5cGUYAyABKAVSB2JpelR5cGUSUgoMY2xpY2tfYnV0dG9uGAQgASgLMi8uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuQ2xpY2tCdXR0b25WMlILY2xpY2tCdXR0b24STAoKdGV4dF9pbnB1dBgFIAEoCzItLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlRleHRJbnB1dFYyUgl0ZXh0SW5wdXQSSQoJY2hlY2tfYm94GAYgASgLMiwuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuQ2hlY2tCb3hWMlIIY2hlY2tCb3gSPwoFdG9hc3QYByABKAsyKS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5Ub2FzdFYyUgV0b2FzdBJCCgZidWJibGUYCCABKAsyKi5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5CdWJibGVWMlIGYnViYmxlEj8KBWxhYmVsGAkgASgLMikuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuTGFiZWxWMlIFbGFiZWwSHwoLcG9zdF9zdGF0dXMYCiABKAVSCnBvc3RTdGF0dXM=');\n@$core.Deprecated('Use responseDescriptor instead')\nconst Response$json = const {\n  '1': 'Response',\n  '2': const [\n    const {'1': 'code', '3': 1, '4': 1, '5': 5, '10': 'code'},\n    const {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'},\n  ],\n};\n\n/// Descriptor for `Response`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List responseDescriptor = $convert.base64Decode(\n    'CghSZXNwb25zZRISCgRjb2RlGAEgASgFUgRjb2RlEhgKB21lc3NhZ2UYAiABKAlSB21lc3NhZ2U=');\n@$core.Deprecated('Use subtitleItemDescriptor instead')\nconst SubtitleItem$json = const {\n  '1': 'SubtitleItem',\n  '2': const [\n    const {'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'},\n    const {'1': 'id_str', '3': 2, '4': 1, '5': 9, '10': 'idStr'},\n    const {'1': 'lan', '3': 3, '4': 1, '5': 9, '10': 'lan'},\n    const {'1': 'lan_doc', '3': 4, '4': 1, '5': 9, '10': 'lanDoc'},\n    const {'1': 'subtitle_url', '3': 5, '4': 1, '5': 9, '10': 'subtitleUrl'},\n    const {\n      '1': 'author',\n      '3': 6,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.UserInfo',\n      '10': 'author'\n    },\n    const {\n      '1': 'type',\n      '3': 7,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.SubtitleType',\n      '10': 'type'\n    },\n    const {'1': 'lan_doc_brief', '3': 8, '4': 1, '5': 9, '10': 'lanDocBrief'},\n    const {\n      '1': 'ai_type',\n      '3': 9,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.SubtitleAiType',\n      '10': 'aiType'\n    },\n    const {\n      '1': 'ai_status',\n      '3': 10,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.SubtitleAiStatus',\n      '10': 'aiStatus'\n    },\n  ],\n};\n\n/// Descriptor for `SubtitleItem`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List subtitleItemDescriptor = $convert.base64Decode(\n    'CgxTdWJ0aXRsZUl0ZW0SDgoCaWQYASABKANSAmlkEhUKBmlkX3N0chgCIAEoCVIFaWRTdHISEAoDbGFuGAMgASgJUgNsYW4SFwoHbGFuX2RvYxgEIAEoCVIGbGFuRG9jEiEKDHN1YnRpdGxlX3VybBgFIAEoCVILc3VidGl0bGVVcmwSQgoGYXV0aG9yGAYgASgLMiouYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuVXNlckluZm9SBmF1dGhvchJCCgR0eXBlGAcgASgOMi4uYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuU3VidGl0bGVUeXBlUgR0eXBlEiIKDWxhbl9kb2NfYnJpZWYYCCABKAlSC2xhbkRvY0JyaWVmEkkKB2FpX3R5cGUYCSABKA4yMC5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5TdWJ0aXRsZUFpVHlwZVIGYWlUeXBlEk8KCWFpX3N0YXR1cxgKIAEoDjIyLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlN1YnRpdGxlQWlTdGF0dXNSCGFpU3RhdHVz');\n@$core.Deprecated('Use textInputDescriptor instead')\nconst TextInput$json = const {\n  '1': 'TextInput',\n  '2': const [\n    const {\n      '1': 'portrait_placeholder',\n      '3': 1,\n      '4': 3,\n      '5': 9,\n      '10': 'portraitPlaceholder'\n    },\n    const {\n      '1': 'landscape_placeholder',\n      '3': 2,\n      '4': 3,\n      '5': 9,\n      '10': 'landscapePlaceholder'\n    },\n    const {\n      '1': 'render_type',\n      '3': 3,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.RenderType',\n      '10': 'renderType'\n    },\n    const {\n      '1': 'placeholder_post',\n      '3': 4,\n      '4': 1,\n      '5': 8,\n      '10': 'placeholderPost'\n    },\n    const {'1': 'show', '3': 5, '4': 1, '5': 8, '10': 'show'},\n    const {\n      '1': 'avatar',\n      '3': 6,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Avatar',\n      '10': 'avatar'\n    },\n    const {\n      '1': 'post_status',\n      '3': 7,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.PostStatus',\n      '10': 'postStatus'\n    },\n    const {\n      '1': 'label',\n      '3': 8,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Label',\n      '10': 'label'\n    },\n  ],\n};\n\n/// Descriptor for `TextInput`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List textInputDescriptor = $convert.base64Decode(\n    'CglUZXh0SW5wdXQSMQoUcG9ydHJhaXRfcGxhY2Vob2xkZXIYASADKAlSE3BvcnRyYWl0UGxhY2Vob2xkZXISMwoVbGFuZHNjYXBlX3BsYWNlaG9sZGVyGAIgAygJUhRsYW5kc2NhcGVQbGFjZWhvbGRlchJNCgtyZW5kZXJfdHlwZRgDIAEoDjIsLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlJlbmRlclR5cGVSCnJlbmRlclR5cGUSKQoQcGxhY2Vob2xkZXJfcG9zdBgEIAEoCFIPcGxhY2Vob2xkZXJQb3N0EhIKBHNob3cYBSABKAhSBHNob3cSQAoGYXZhdGFyGAYgAygLMiguYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuQXZhdGFyUgZhdmF0YXISTQoLcG9zdF9zdGF0dXMYByABKA4yLC5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5Qb3N0U3RhdHVzUgpwb3N0U3RhdHVzEj0KBWxhYmVsGAggASgLMicuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuTGFiZWxSBWxhYmVs');\n@$core.Deprecated('Use textInputV2Descriptor instead')\nconst TextInputV2$json = const {\n  '1': 'TextInputV2',\n  '2': const [\n    const {\n      '1': 'portrait_placeholder',\n      '3': 1,\n      '4': 3,\n      '5': 9,\n      '10': 'portraitPlaceholder'\n    },\n    const {\n      '1': 'landscape_placeholder',\n      '3': 2,\n      '4': 3,\n      '5': 9,\n      '10': 'landscapePlaceholder'\n    },\n    const {\n      '1': 'render_type',\n      '3': 3,\n      '4': 1,\n      '5': 14,\n      '6': '.bilibili.community.service.dm.v1.RenderType',\n      '10': 'renderType'\n    },\n    const {\n      '1': 'placeholder_post',\n      '3': 4,\n      '4': 1,\n      '5': 8,\n      '10': 'placeholderPost'\n    },\n    const {\n      '1': 'avatar',\n      '3': 5,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Avatar',\n      '10': 'avatar'\n    },\n    const {\n      '1': 'text_input_limit',\n      '3': 6,\n      '4': 1,\n      '5': 5,\n      '10': 'textInputLimit'\n    },\n  ],\n};\n\n/// Descriptor for `TextInputV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List textInputV2Descriptor = $convert.base64Decode(\n    'CgtUZXh0SW5wdXRWMhIxChRwb3J0cmFpdF9wbGFjZWhvbGRlchgBIAMoCVITcG9ydHJhaXRQbGFjZWhvbGRlchIzChVsYW5kc2NhcGVfcGxhY2Vob2xkZXIYAiADKAlSFGxhbmRzY2FwZVBsYWNlaG9sZGVyEk0KC3JlbmRlcl90eXBlGAMgASgOMiwuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUmVuZGVyVHlwZVIKcmVuZGVyVHlwZRIpChBwbGFjZWhvbGRlcl9wb3N0GAQgASgIUg9wbGFjZWhvbGRlclBvc3QSQAoGYXZhdGFyGAUgAygLMiguYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuQXZhdGFyUgZhdmF0YXISKAoQdGV4dF9pbnB1dF9saW1pdBgGIAEoBVIOdGV4dElucHV0TGltaXQ=');\n@$core.Deprecated('Use toastDescriptor instead')\nconst Toast$json = const {\n  '1': 'Toast',\n  '2': const [\n    const {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},\n    const {'1': 'duration', '3': 2, '4': 1, '5': 5, '10': 'duration'},\n    const {'1': 'show', '3': 3, '4': 1, '5': 8, '10': 'show'},\n    const {\n      '1': 'button',\n      '3': 4,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.Button',\n      '10': 'button'\n    },\n  ],\n};\n\n/// Descriptor for `Toast`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List toastDescriptor = $convert.base64Decode(\n    'CgVUb2FzdBISCgR0ZXh0GAEgASgJUgR0ZXh0EhoKCGR1cmF0aW9uGAIgASgFUghkdXJhdGlvbhISCgRzaG93GAMgASgIUgRzaG93EkAKBmJ1dHRvbhgEIAEoCzIoLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkJ1dHRvblIGYnV0dG9u');\n@$core.Deprecated('Use toastButtonV2Descriptor instead')\nconst ToastButtonV2$json = const {\n  '1': 'ToastButtonV2',\n  '2': const [\n    const {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},\n    const {'1': 'action', '3': 2, '4': 1, '5': 5, '10': 'action'},\n  ],\n};\n\n/// Descriptor for `ToastButtonV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List toastButtonV2Descriptor = $convert.base64Decode(\n    'Cg1Ub2FzdEJ1dHRvblYyEhIKBHRleHQYASABKAlSBHRleHQSFgoGYWN0aW9uGAIgASgFUgZhY3Rpb24=');\n@$core.Deprecated('Use toastV2Descriptor instead')\nconst ToastV2$json = const {\n  '1': 'ToastV2',\n  '2': const [\n    const {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},\n    const {'1': 'duration', '3': 2, '4': 1, '5': 5, '10': 'duration'},\n    const {\n      '1': 'toast_button_v2',\n      '3': 3,\n      '4': 1,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.ToastButtonV2',\n      '10': 'toastButtonV2'\n    },\n  ],\n};\n\n/// Descriptor for `ToastV2`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List toastV2Descriptor = $convert.base64Decode(\n    'CgdUb2FzdFYyEhIKBHRleHQYASABKAlSBHRleHQSGgoIZHVyYXRpb24YAiABKAVSCGR1cmF0aW9uElcKD3RvYXN0X2J1dHRvbl92MhgDIAEoCzIvLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlRvYXN0QnV0dG9uVjJSDXRvYXN0QnV0dG9uVjI=');\n@$core.Deprecated('Use userInfoDescriptor instead')\nconst UserInfo$json = const {\n  '1': 'UserInfo',\n  '2': const [\n    const {'1': 'mid', '3': 1, '4': 1, '5': 3, '10': 'mid'},\n    const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},\n    const {'1': 'sex', '3': 3, '4': 1, '5': 9, '10': 'sex'},\n    const {'1': 'face', '3': 4, '4': 1, '5': 9, '10': 'face'},\n    const {'1': 'sign', '3': 5, '4': 1, '5': 9, '10': 'sign'},\n    const {'1': 'rank', '3': 6, '4': 1, '5': 5, '10': 'rank'},\n  ],\n};\n\n/// Descriptor for `UserInfo`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List userInfoDescriptor = $convert.base64Decode(\n    'CghVc2VySW5mbxIQCgNtaWQYASABKANSA21pZBISCgRuYW1lGAIgASgJUgRuYW1lEhAKA3NleBgDIAEoCVIDc2V4EhIKBGZhY2UYBCABKAlSBGZhY2USEgoEc2lnbhgFIAEoCVIEc2lnbhISCgRyYW5rGAYgASgFUgRyYW5r');\n@$core.Deprecated('Use videoMaskDescriptor instead')\nconst VideoMask$json = const {\n  '1': 'VideoMask',\n  '2': const [\n    const {'1': 'cid', '3': 1, '4': 1, '5': 3, '10': 'cid'},\n    const {'1': 'plat', '3': 2, '4': 1, '5': 5, '10': 'plat'},\n    const {'1': 'fps', '3': 3, '4': 1, '5': 5, '10': 'fps'},\n    const {'1': 'time', '3': 4, '4': 1, '5': 3, '10': 'time'},\n    const {'1': 'mask_url', '3': 5, '4': 1, '5': 9, '10': 'maskUrl'},\n  ],\n};\n\n/// Descriptor for `VideoMask`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List videoMaskDescriptor = $convert.base64Decode(\n    'CglWaWRlb01hc2sSEAoDY2lkGAEgASgDUgNjaWQSEgoEcGxhdBgCIAEoBVIEcGxhdBIQCgNmcHMYAyABKAVSA2ZwcxISCgR0aW1lGAQgASgDUgR0aW1lEhkKCG1hc2tfdXJsGAUgASgJUgdtYXNrVXJs');\n@$core.Deprecated('Use videoSubtitleDescriptor instead')\nconst VideoSubtitle$json = const {\n  '1': 'VideoSubtitle',\n  '2': const [\n    const {'1': 'lan', '3': 1, '4': 1, '5': 9, '10': 'lan'},\n    const {'1': 'lanDoc', '3': 2, '4': 1, '5': 9, '10': 'lanDoc'},\n    const {\n      '1': 'subtitles',\n      '3': 3,\n      '4': 3,\n      '5': 11,\n      '6': '.bilibili.community.service.dm.v1.SubtitleItem',\n      '10': 'subtitles'\n    },\n  ],\n};\n\n/// Descriptor for `VideoSubtitle`. Decode as a `google.protobuf.DescriptorProto`.\nfinal $typed_data.Uint8List videoSubtitleDescriptor = $convert.base64Decode(\n    'Cg1WaWRlb1N1YnRpdGxlEhAKA2xhbhgBIAEoCVIDbGFuEhYKBmxhbkRvYxgCIAEoCVIGbGFuRG9jEkwKCXN1YnRpdGxlcxgDIAMoCzIuLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLlN1YnRpdGxlSXRlbVIJc3VidGl0bGVz');\nconst $core.Map<$core.String, $core.dynamic> DMServiceBase$json = const {\n  '1': 'DM',\n  '2': const [\n    const {\n      '1': 'DmSegMobile',\n      '2': '.bilibili.community.service.dm.v1.DmSegMobileReq',\n      '3': '.bilibili.community.service.dm.v1.DmSegMobileReply'\n    },\n    const {\n      '1': 'DmView',\n      '2': '.bilibili.community.service.dm.v1.DmViewReq',\n      '3': '.bilibili.community.service.dm.v1.DmViewReply'\n    },\n    const {\n      '1': 'DmPlayerConfig',\n      '2': '.bilibili.community.service.dm.v1.DmPlayerConfigReq',\n      '3': '.bilibili.community.service.dm.v1.Response'\n    },\n    const {\n      '1': 'DmSegOtt',\n      '2': '.bilibili.community.service.dm.v1.DmSegOttReq',\n      '3': '.bilibili.community.service.dm.v1.DmSegOttReply'\n    },\n    const {\n      '1': 'DmSegSDK',\n      '2': '.bilibili.community.service.dm.v1.DmSegSDKReq',\n      '3': '.bilibili.community.service.dm.v1.DmSegSDKReply'\n    },\n    const {\n      '1': 'DmExpoReport',\n      '2': '.bilibili.community.service.dm.v1.DmExpoReportReq',\n      '3': '.bilibili.community.service.dm.v1.DmExpoReportRes'\n    },\n  ],\n};\n\n@$core.Deprecated('Use dMServiceDescriptor instead')\nconst $core.Map<$core.String, $core.Map<$core.String, $core.dynamic>>\n    DMServiceBase$messageJson = const {\n  '.bilibili.community.service.dm.v1.DmSegMobileReq': DmSegMobileReq$json,\n  '.bilibili.community.service.dm.v1.DmSegMobileReply': DmSegMobileReply$json,\n  '.bilibili.community.service.dm.v1.DanmakuElem': DanmakuElem$json,\n  '.bilibili.community.service.dm.v1.DanmakuAIFlag': DanmakuAIFlag$json,\n  '.bilibili.community.service.dm.v1.DanmakuFlag': DanmakuFlag$json,\n  '.bilibili.community.service.dm.v1.DmViewReq': DmViewReq$json,\n  '.bilibili.community.service.dm.v1.DmViewReply': DmViewReply$json,\n  '.bilibili.community.service.dm.v1.VideoMask': VideoMask$json,\n  '.bilibili.community.service.dm.v1.VideoSubtitle': VideoSubtitle$json,\n  '.bilibili.community.service.dm.v1.SubtitleItem': SubtitleItem$json,\n  '.bilibili.community.service.dm.v1.UserInfo': UserInfo$json,\n  '.bilibili.community.service.dm.v1.DanmakuFlagConfig': DanmakuFlagConfig$json,\n  '.bilibili.community.service.dm.v1.DanmuPlayerViewConfig':\n      DanmuPlayerViewConfig$json,\n  '.bilibili.community.service.dm.v1.DanmuDefaultPlayerConfig':\n      DanmuDefaultPlayerConfig$json,\n  '.bilibili.community.service.dm.v1.DanmuDefaultPlayerConfig.PlayerDanmakuAiRecommendedLevelV2MapEntry':\n      DanmuDefaultPlayerConfig_PlayerDanmakuAiRecommendedLevelV2MapEntry$json,\n  '.bilibili.community.service.dm.v1.DanmuPlayerConfig': DanmuPlayerConfig$json,\n  '.bilibili.community.service.dm.v1.DanmuPlayerConfig.PlayerDanmakuAiRecommendedLevelV2MapEntry':\n      DanmuPlayerConfig_PlayerDanmakuAiRecommendedLevelV2MapEntry$json,\n  '.bilibili.community.service.dm.v1.DanmuPlayerDynamicConfig':\n      DanmuPlayerDynamicConfig$json,\n  '.bilibili.community.service.dm.v1.DanmuPlayerConfigPanel':\n      DanmuPlayerConfigPanel$json,\n  '.bilibili.community.service.dm.v1.ExpoReport': ExpoReport$json,\n  '.bilibili.community.service.dm.v1.BuzzwordConfig': BuzzwordConfig$json,\n  '.bilibili.community.service.dm.v1.BuzzwordShowConfig':\n      BuzzwordShowConfig$json,\n  '.bilibili.community.service.dm.v1.Expressions': Expressions$json,\n  '.bilibili.community.service.dm.v1.Expression': Expression$json,\n  '.bilibili.community.service.dm.v1.Period': Period$json,\n  '.bilibili.community.service.dm.v1.PostPanel': PostPanel$json,\n  '.bilibili.community.service.dm.v1.ClickButton': ClickButton$json,\n  '.bilibili.community.service.dm.v1.Bubble': Bubble$json,\n  '.bilibili.community.service.dm.v1.TextInput': TextInput$json,\n  '.bilibili.community.service.dm.v1.Avatar': Avatar$json,\n  '.bilibili.community.service.dm.v1.Label': Label$json,\n  '.bilibili.community.service.dm.v1.CheckBox': CheckBox$json,\n  '.bilibili.community.service.dm.v1.Toast': Toast$json,\n  '.bilibili.community.service.dm.v1.Button': Button$json,\n  '.bilibili.community.service.dm.v1.PostPanelV2': PostPanelV2$json,\n  '.bilibili.community.service.dm.v1.ClickButtonV2': ClickButtonV2$json,\n  '.bilibili.community.service.dm.v1.TextInputV2': TextInputV2$json,\n  '.bilibili.community.service.dm.v1.CheckBoxV2': CheckBoxV2$json,\n  '.bilibili.community.service.dm.v1.ToastV2': ToastV2$json,\n  '.bilibili.community.service.dm.v1.ToastButtonV2': ToastButtonV2$json,\n  '.bilibili.community.service.dm.v1.BubbleV2': BubbleV2$json,\n  '.bilibili.community.service.dm.v1.LabelV2': LabelV2$json,\n  '.bilibili.community.service.dm.v1.DmPlayerConfigReq': DmPlayerConfigReq$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuSwitch':\n      PlayerDanmakuSwitch$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuSwitchSave':\n      PlayerDanmakuSwitchSave$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuUseDefaultConfig':\n      PlayerDanmakuUseDefaultConfig$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedSwitch':\n      PlayerDanmakuAiRecommendedSwitch$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedLevel':\n      PlayerDanmakuAiRecommendedLevel$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuBlocktop':\n      PlayerDanmakuBlocktop$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuBlockscroll':\n      PlayerDanmakuBlockscroll$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuBlockbottom':\n      PlayerDanmakuBlockbottom$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuBlockcolorful':\n      PlayerDanmakuBlockcolorful$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuBlockrepeat':\n      PlayerDanmakuBlockrepeat$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuBlockspecial':\n      PlayerDanmakuBlockspecial$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuOpacity':\n      PlayerDanmakuOpacity$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuScalingfactor':\n      PlayerDanmakuScalingfactor$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuDomain':\n      PlayerDanmakuDomain$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuSpeed':\n      PlayerDanmakuSpeed$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuEnableblocklist':\n      PlayerDanmakuEnableblocklist$json,\n  '.bilibili.community.service.dm.v1.InlinePlayerDanmakuSwitch':\n      InlinePlayerDanmakuSwitch$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuSeniorModeSwitch':\n      PlayerDanmakuSeniorModeSwitch$json,\n  '.bilibili.community.service.dm.v1.PlayerDanmakuAiRecommendedLevelV2':\n      PlayerDanmakuAiRecommendedLevelV2$json,\n  '.bilibili.community.service.dm.v1.Response': Response$json,\n  '.bilibili.community.service.dm.v1.DmSegOttReq': DmSegOttReq$json,\n  '.bilibili.community.service.dm.v1.DmSegOttReply': DmSegOttReply$json,\n  '.bilibili.community.service.dm.v1.DmSegSDKReq': DmSegSDKReq$json,\n  '.bilibili.community.service.dm.v1.DmSegSDKReply': DmSegSDKReply$json,\n  '.bilibili.community.service.dm.v1.DmExpoReportReq': DmExpoReportReq$json,\n  '.bilibili.community.service.dm.v1.DmExpoReportRes': DmExpoReportRes$json,\n};\n\n/// Descriptor for `DM`. Decode as a `google.protobuf.ServiceDescriptorProto`.\nfinal $typed_data.Uint8List dMServiceDescriptor = $convert.base64Decode(\n    'CgJETRJzCgtEbVNlZ01vYmlsZRIwLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRtU2VnTW9iaWxlUmVxGjIuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRG1TZWdNb2JpbGVSZXBseRJkCgZEbVZpZXcSKy5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5EbVZpZXdSZXEaLS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5EbVZpZXdSZXBseRJxCg5EbVBsYXllckNvbmZpZxIzLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRtUGxheWVyQ29uZmlnUmVxGiouYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuUmVzcG9uc2USagoIRG1TZWdPdHQSLS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5EbVNlZ090dFJlcRovLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRtU2VnT3R0UmVwbHkSagoIRG1TZWdTREsSLS5iaWxpYmlsaS5jb21tdW5pdHkuc2VydmljZS5kbS52MS5EbVNlZ1NES1JlcRovLmJpbGliaWxpLmNvbW11bml0eS5zZXJ2aWNlLmRtLnYxLkRtU2VnU0RLUmVwbHkSdAoMRG1FeHBvUmVwb3J0EjEuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRG1FeHBvUmVwb3J0UmVxGjEuYmlsaWJpbGkuY29tbXVuaXR5LnNlcnZpY2UuZG0udjEuRG1FeHBvUmVwb3J0UmVz');\n"
  },
  {
    "path": "lib/models/danmaku/dm.pbserver.dart",
    "content": "///\n//  Generated code. Do not modify.\n//  source: dm.proto\n//\n// @dart = 2.12\n// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name, avoid_renaming_method_parameters\n\nimport 'dart:async' as $async;\n\nimport 'package:protobuf/protobuf.dart' as $pb;\n\nimport 'dart:core' as $core;\nimport 'dm.pb.dart' as $0;\nimport 'dm.pbjson.dart';\n\nexport 'dm.pb.dart';\n\nabstract class DMServiceBase extends $pb.GeneratedService {\n  $async.Future<$0.DmSegMobileReply> dmSegMobile(\n      $pb.ServerContext ctx, $0.DmSegMobileReq request);\n  $async.Future<$0.DmViewReply> dmView(\n      $pb.ServerContext ctx, $0.DmViewReq request);\n  $async.Future<$0.Response> dmPlayerConfig(\n      $pb.ServerContext ctx, $0.DmPlayerConfigReq request);\n  $async.Future<$0.DmSegOttReply> dmSegOtt(\n      $pb.ServerContext ctx, $0.DmSegOttReq request);\n  $async.Future<$0.DmSegSDKReply> dmSegSDK(\n      $pb.ServerContext ctx, $0.DmSegSDKReq request);\n  $async.Future<$0.DmExpoReportRes> dmExpoReport(\n      $pb.ServerContext ctx, $0.DmExpoReportReq request);\n\n  $pb.GeneratedMessage createRequest($core.String method) {\n    switch (method) {\n      case 'DmSegMobile':\n        return $0.DmSegMobileReq();\n      case 'DmView':\n        return $0.DmViewReq();\n      case 'DmPlayerConfig':\n        return $0.DmPlayerConfigReq();\n      case 'DmSegOtt':\n        return $0.DmSegOttReq();\n      case 'DmSegSDK':\n        return $0.DmSegSDKReq();\n      case 'DmExpoReport':\n        return $0.DmExpoReportReq();\n      default:\n        throw $core.ArgumentError('Unknown method: $method');\n    }\n  }\n\n  $async.Future<$pb.GeneratedMessage> handleCall($pb.ServerContext ctx,\n      $core.String method, $pb.GeneratedMessage request) {\n    switch (method) {\n      case 'DmSegMobile':\n        return this.dmSegMobile(ctx, request as $0.DmSegMobileReq);\n      case 'DmView':\n        return this.dmView(ctx, request as $0.DmViewReq);\n      case 'DmPlayerConfig':\n        return this.dmPlayerConfig(ctx, request as $0.DmPlayerConfigReq);\n      case 'DmSegOtt':\n        return this.dmSegOtt(ctx, request as $0.DmSegOttReq);\n      case 'DmSegSDK':\n        return this.dmSegSDK(ctx, request as $0.DmSegSDKReq);\n      case 'DmExpoReport':\n        return this.dmExpoReport(ctx, request as $0.DmExpoReportReq);\n      default:\n        throw $core.ArgumentError('Unknown method: $method');\n    }\n  }\n\n  $core.Map<$core.String, $core.dynamic> get $json => DMServiceBase$json;\n  $core.Map<$core.String, $core.Map<$core.String, $core.dynamic>>\n      get $messageJson => DMServiceBase$messageJson;\n}\n"
  },
  {
    "path": "lib/models/danmaku/dm.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.community.service.dm.v1;\n\n// 说明文档\n// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/danmaku/danmaku_proto.md\n\n//弹幕\nservice DM {\n    // 获取分段弹幕\n    rpc DmSegMobile (DmSegMobileReq) returns (DmSegMobileReply);\n    // 客户端弹幕元数据 字幕、分段、防挡蒙版等\n    rpc DmView(DmViewReq) returns (DmViewReply);\n    // 修改弹幕配置\n    rpc DmPlayerConfig (DmPlayerConfigReq) returns (Response);\n    // ott弹幕列表\n    rpc DmSegOtt(DmSegOttReq) returns(DmSegOttReply);\n    // SDK弹幕列表\n    rpc DmSegSDK(DmSegSDKReq) returns(DmSegSDKReply);\n    //\n    rpc DmExpoReport(DmExpoReportReq) returns (DmExpoReportRes);\n}\n\n//\nmessage Avatar {\n    //\n    string id = 1;\n    //\n    string url = 2;\n    //\n    AvatarType avatar_type = 3;\n}\n\n//\nenum AvatarType {\n    AvatarTypeNone = 0; //\n    AvatarTypeNFT  = 1; //\n}\n\n//\nmessage Bubble {\n    //\n    string text = 1;\n    //\n    string url = 2;\n}\n\n//\nenum BubbleType {\n    BubbleTypeNone           = 0; //\n    BubbleTypeClickButton    = 1; //\n    BubbleTypeDmSettingPanel = 2; //\n}\n\n//\nmessage BubbleV2 {\n    //\n    string text = 1;\n    //\n    string url = 2;\n    //\n    BubbleType bubble_type = 3;\n    //\n    bool exposure_once = 4;\n    //\n    ExposureType exposure_type = 5;\n}\n\n//\nmessage Button {\n    //\n    string text = 1;\n    //\n    int32 action = 2;\n}\n\n//\nmessage BuzzwordConfig {\n    //\n    repeated BuzzwordShowConfig keywords = 1;\n}\n\n//\nmessage BuzzwordShowConfig {\n    //\n    string name = 1;\n    //\n    string schema = 2;\n    //\n    int32 source = 3;\n    //\n    int64 id = 4;\n    //\n    int64 buzzword_id = 5;\n    //\n    int32 schema_type = 6;\n}\n\n//\nmessage CheckBox {\n    //\n    string text = 1;\n    //\n    CheckboxType type = 2;\n    //\n    bool default_value = 3;\n    //\n    bool show = 4;\n}\n\n//\nenum CheckboxType {\n    CheckboxTypeNone      = 0; //\n    CheckboxTypeEncourage = 1; //\n    CheckboxTypeColorDM   = 2; //\n}\n\n//\nmessage CheckBoxV2 {\n    //\n    string text = 1;\n    //\n    int32 type = 2;\n    //\n    bool default_value = 3;\n}\n\n//\nmessage ClickButton {\n    //\n    repeated string portrait_text = 1;\n    //\n    repeated string landscape_text = 2;\n    //\n    repeated string portrait_text_focus = 3;\n    //\n    repeated string landscape_text_focus = 4;\n    //\n    RenderType render_type = 5;\n    //\n    bool show = 6;\n    //\n    Bubble bubble = 7;\n}\n\n//\nmessage ClickButtonV2 {\n    //\n    repeated string portrait_text = 1;\n    //\n    repeated string landscape_text = 2;\n    //\n    repeated string portrait_text_focus = 3;\n    //\n    repeated string landscape_text_focus = 4;\n    //\n    int32 render_type = 5;\n    //\n    bool text_input_post = 6;\n    //\n    bool exposure_once = 7;\n    //\n    int32 exposure_type = 8;\n}\n\n// 互动弹幕条目信息\nmessage CommandDm {\n    // 弹幕id\n    int64 id = 1;\n    // 对象视频cid\n    int64 oid = 2;\n    // 发送者mid\n    string mid = 3;\n    // 互动弹幕指令\n    string command = 4;\n    // 互动弹幕正文\n    string content = 5;\n    // 出现时间\n    int32 progress = 6;\n    // 创建时间\n    string ctime = 7;\n    // 发布时间\n    string mtime = 8;\n    // 扩展json数据\n    string extra = 9;\n    // 弹幕id str类型\n    string idStr = 10;\n}\n\n// 弹幕ai云屏蔽列表\nmessage DanmakuAIFlag {\n    // 弹幕ai云屏蔽条目\n    repeated DanmakuFlag dm_flags = 1;\n}\n\n// 弹幕条目\nmessage DanmakuElem {\n    // 弹幕dmid\n    int64 id = 1;\n    // 弹幕出现位置(单位ms)\n    int32 progress = 2;\n    // 弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2)\n    int32 mode = 3;\n    // 弹幕字号\n    int32 fontsize = 4;\n    // 弹幕颜色\n    uint32 color = 5;\n    // 发送者mid hash\n    string midHash = 6;\n    // 弹幕正文\n    string content = 7;\n    // 发送时间\n    int64 ctime = 8;\n    // 权重 用于屏蔽等级 区间:[1,10]\n    int32 weight = 9;\n    // 动作\n    string action = 10;\n    // 弹幕池 0:普通池 1:字幕池 2:特殊池(代码/BAS弹幕)\n    int32 pool = 11;\n    // 弹幕dmid str\n    string idStr = 12;\n    // 弹幕属性位(bin求AND)\n    // bit0:保护 bit1:直播 bit2:高赞\n    int32 attr = 13;\n    //\n    string animation = 22;\n    // 大会员专属颜色\n    DmColorfulType colorful = 24;\n}\n\n// 弹幕ai云屏蔽条目\nmessage DanmakuFlag {\n    // 弹幕dmid\n    int64 dmid = 1;\n    // 评分\n    uint32 flag = 2;\n}\n\n// 云屏蔽配置信息\nmessage DanmakuFlagConfig {\n    // 云屏蔽等级\n    int32 rec_flag = 1;\n    // 云屏蔽文案\n    string rec_text = 2;\n    // 云屏蔽开关\n    int32 rec_switch = 3;\n}\n\n// 弹幕默认配置\nmessage DanmuDefaultPlayerConfig {\n    bool player_danmaku_use_default_config                       = 1;  // 是否使用推荐弹幕设置\n    bool player_danmaku_ai_recommended_switch                    = 4;  // 是否开启智能云屏蔽\n    int32 player_danmaku_ai_recommended_level                    = 5;  // 智能云屏蔽等级\n    bool player_danmaku_blocktop                                 = 6;  // 是否屏蔽顶端弹幕\n    bool player_danmaku_blockscroll                              = 7;  // 是否屏蔽滚动弹幕\n    bool player_danmaku_blockbottom                              = 8;  // 是否屏蔽底端弹幕\n    bool player_danmaku_blockcolorful                            = 9;  // 是否屏蔽彩色弹幕\n    bool player_danmaku_blockrepeat                              = 10; // 是否屏蔽重复弹幕\n    bool player_danmaku_blockspecial                             = 11; // 是否屏蔽高级弹幕\n    float player_danmaku_opacity                                 = 12; // 弹幕不透明度\n    float player_danmaku_scalingfactor                           = 13; // 弹幕缩放比例\n    float player_danmaku_domain                                  = 14; // 弹幕显示区域\n    int32 player_danmaku_speed                                   = 15; // 弹幕速度\n    bool inline_player_danmaku_switch                            = 16; // 是否开启弹幕\n    int32 player_danmaku_senior_mode_switch                      = 17; //\n    int32 player_danmaku_ai_recommended_level_v2                 = 18; //\n    map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 19; //\n}\n\n// 弹幕配置\nmessage DanmuPlayerConfig {\n    bool player_danmaku_switch                                   = 1;  // 是否开启弹幕\n    bool player_danmaku_switch_save                              = 2;  // 是否记录弹幕开关设置\n    bool player_danmaku_use_default_config                       = 3;  // 是否使用推荐弹幕设置\n    bool player_danmaku_ai_recommended_switch                    = 4;  // 是否开启智能云屏蔽\n    int32 player_danmaku_ai_recommended_level                    = 5;  // 智能云屏蔽等级\n    bool player_danmaku_blocktop                                 = 6;  // 是否屏蔽顶端弹幕\n    bool player_danmaku_blockscroll                              = 7;  // 是否屏蔽滚动弹幕\n    bool player_danmaku_blockbottom                              = 8;  // 是否屏蔽底端弹幕\n    bool player_danmaku_blockcolorful                            = 9;  // 是否屏蔽彩色弹幕\n    bool player_danmaku_blockrepeat                              = 10; // 是否屏蔽重复弹幕\n    bool player_danmaku_blockspecial                             = 11; // 是否屏蔽高级弹幕\n    float player_danmaku_opacity                                 = 12; // 弹幕不透明度\n    float player_danmaku_scalingfactor                           = 13; // 弹幕缩放比例\n    float player_danmaku_domain                                  = 14; // 弹幕显示区域\n    int32 player_danmaku_speed                                   = 15; // 弹幕速度\n    bool player_danmaku_enableblocklist                          = 16; // 是否开启屏蔽列表\n    bool inline_player_danmaku_switch                            = 17; // 是否开启弹幕\n    int32 inline_player_danmaku_config                           = 18; //\n    int32 player_danmaku_ios_switch_save                         = 19; //\n    int32 player_danmaku_senior_mode_switch                      = 20; //\n    int32 player_danmaku_ai_recommended_level_v2                 = 21; //\n    map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 22; //\n}\n\n//\nmessage DanmuPlayerConfigPanel {\n    //\n    string selection_text = 1;\n}\n\n// 弹幕显示区域自动配置\nmessage DanmuPlayerDynamicConfig {\n    // 时间\n    int32 progress = 1;\n    // 弹幕显示区域\n    float player_danmaku_domain = 14;\n}\n\n// 弹幕配置信息\nmessage DanmuPlayerViewConfig {\n    // 弹幕默认配置\n    DanmuDefaultPlayerConfig danmuku_default_player_config = 1;\n    // 弹幕用户配置\n    DanmuPlayerConfig danmuku_player_config = 2;\n    // 弹幕显示区域自动配置列表\n    repeated DanmuPlayerDynamicConfig danmuku_player_dynamic_config = 3;\n    //\n    DanmuPlayerConfigPanel danmuku_player_config_panel = 4;\n}\n\n// web端用户弹幕配置\nmessage DanmuWebPlayerConfig {\n    bool dm_switch                    = 1;  // 是否开启弹幕\n    bool ai_switch                    = 2;  // 是否开启智能云屏蔽\n    int32 ai_level                    = 3;  // 智能云屏蔽等级\n    bool blocktop                     = 4;  // 是否屏蔽顶端弹幕\n    bool blockscroll                  = 5;  // 是否屏蔽滚动弹幕\n    bool blockbottom                  = 6;  // 是否屏蔽底端弹幕\n    bool blockcolor                   = 7;  // 是否屏蔽彩色弹幕\n    bool blockspecial                 = 8;  // 是否屏蔽重复弹幕\n    bool preventshade                 = 9;  // \n    bool dmask                        = 10; // \n    float opacity                     = 11; // \n    int32 dmarea                      = 12; // \n    float speedplus                   = 13; // \n    float fontsize                    = 14; // 弹幕字号\n    bool screensync                   = 15; // \n    bool speedsync                    = 16; // \n    string fontfamily                 = 17; // \n    bool bold                         = 18; // 是否使用加粗\n    int32 fontborder                  = 19; // \n    string draw_type                  = 20; // 弹幕渲染类型\n    int32 senior_mode_switch          = 21; //\n    int32 ai_level_v2                 = 22; //\n    map<int32, int32> ai_level_v2_map = 23; //\n}\n\n// 弹幕属性位值\nenum DMAttrBit {\n    DMAttrBitProtect  = 0; // 保护弹幕\n    DMAttrBitFromLive = 1; // 直播弹幕\n    DMAttrHighLike    = 2; // 高赞弹幕\n}\n\nmessage DmColorful {\n    DmColorfulType type = 1; // 颜色类型\n    string src          = 2; //\n}\n\nenum DmColorfulType {\n    NoneType        = 0;     // 无\n    VipGradualColor = 60001; // 渐变色\n}\n\n//\nmessage DmExpoReportReq {\n    //\n    string session_id = 1;\n    //\n    int64 oid = 2;\n    //\n    string spmid = 4;\n}\n\n//\nmessage DmExpoReportRes {}\n\n// 修改弹幕配置-请求\nmessage DmPlayerConfigReq {\n    int64 ts                                                  = 1;  //\n    PlayerDanmakuSwitch switch                                = 2;  // 是否开启弹幕\n    PlayerDanmakuSwitchSave switch_save                       = 3;  // 是否记录弹幕开关设置\n    PlayerDanmakuUseDefaultConfig use_default_config          = 4;  // 是否使用推荐弹幕设置\n    PlayerDanmakuAiRecommendedSwitch ai_recommended_switch    = 5;  // 是否开启智能云屏蔽\n    PlayerDanmakuAiRecommendedLevel ai_recommended_level      = 6;  // 智能云屏蔽等级\n    PlayerDanmakuBlocktop blocktop                            = 7;  // 是否屏蔽顶端弹幕\n    PlayerDanmakuBlockscroll blockscroll                      = 8;  // 是否屏蔽滚动弹幕\n    PlayerDanmakuBlockbottom blockbottom                      = 9;  // 是否屏蔽底端弹幕\n    PlayerDanmakuBlockcolorful blockcolorful                  = 10; // 是否屏蔽彩色弹幕\n    PlayerDanmakuBlockrepeat blockrepeat                      = 11; // 是否屏蔽重复弹幕\n    PlayerDanmakuBlockspecial blockspecial                    = 12; // 是否屏蔽高级弹幕\n    PlayerDanmakuOpacity opacity                              = 13; // 弹幕不透明度\n    PlayerDanmakuScalingfactor scalingfactor                  = 14; // 弹幕缩放比例\n    PlayerDanmakuDomain domain                                = 15; // 弹幕显示区域\n    PlayerDanmakuSpeed speed                                  = 16; // 弹幕速度\n    PlayerDanmakuEnableblocklist enableblocklist              = 17; // 是否开启屏蔽列表\n    InlinePlayerDanmakuSwitch inlinePlayerDanmakuSwitch       = 18; // 是否开启弹幕\n    PlayerDanmakuSeniorModeSwitch senior_mode_switch          = 19; //\n    PlayerDanmakuAiRecommendedLevelV2 ai_recommended_level_v2 = 20; //\n}\n\n//\nmessage DmSegConfig {\n    //\n    int64 page_size = 1;\n    //\n    int64 total = 2;\n}\n\n// 获取弹幕-响应\nmessage DmSegMobileReply {\n    // 弹幕列表\n    repeated DanmakuElem elems = 1;\n    // 是否已关闭弹幕\n    // 0:未关闭 1:已关闭\n    int32 state = 2;\n    // 弹幕云屏蔽ai评分值\n    DanmakuAIFlag ai_flag = 3;\n    repeated DmColorful colorfulSrc = 5;\n}\n\n// 获取弹幕-请求\nmessage DmSegMobileReq {\n    // 稿件avid/漫画epid\n    int64 pid = 1;\n    // 视频cid/漫画cid\n    int64 oid = 2;\n    // 弹幕类型\n    // 1:视频 2:漫画\n    int32 type = 3;\n    // 分段(6min)\n    int64 segment_index = 4;\n    // 是否青少年模式\n    int32 teenagers_mode = 5;\n    //\n    int64 ps = 6;\n    //\n    int64 pe = 7;\n    //\n    int32 pull_mode = 8;\n    //\n    int32 from_scene = 9;\n}\n\n// ott弹幕列表-响应\nmessage DmSegOttReply {\n    // 是否已关闭弹幕\n    // 0:未关闭 1:已关闭\n    bool closed = 1;\n    // 弹幕列表\n    repeated DanmakuElem elems = 2;\n}\n\n// ott弹幕列表-请求\nmessage DmSegOttReq {\n    // 稿件avid/漫画epid\n    int64 pid = 1;\n    // 视频cid/漫画cid\n    int64 oid = 2;\n    // 弹幕类型\n    // 1:视频 2:漫画\n    int32 type = 3;\n    // 分段(6min)\n    int64 segment_index = 4;\n}\n\n// 弹幕SDK-响应\nmessage DmSegSDKReply {\n    // 是否已关闭弹幕\n    // 0:未关闭 1:已关闭\n    bool closed = 1;\n    // 弹幕列表\n    repeated DanmakuElem elems = 2;\n}\n\n// 弹幕SDK-请求\nmessage DmSegSDKReq {\n    // 稿件avid/漫画epid\n    int64 pid = 1;\n    // 视频cid/漫画cid\n    int64 oid = 2;\n    // 弹幕类型\n    // 1:视频 2:漫画\n    int32 type = 3;\n    // 分段(6min)\n    int64 segment_index = 4;\n}\n\n// 客户端弹幕元数据-响应\nmessage DmViewReply {\n    // 是否已关闭弹幕\n    // 0:未关闭 1:已关闭\n    bool closed = 1;\n    // 智能防挡弹幕蒙版信息\n    VideoMask mask = 2;\n    // 视频字幕\n    VideoSubtitle subtitle = 3;\n    // 高级弹幕专包url(bfs)\n    repeated string special_dms = 4;\n    // 云屏蔽配置信息\n    DanmakuFlagConfig ai_flag = 5;\n    // 弹幕配置信息\n    DanmuPlayerViewConfig player_config = 6;\n    // 弹幕发送框样式\n    int32 send_box_style = 7;\n    // 是否允许\n    bool allow = 8;\n    // check box 是否展示\n    string check_box = 9;\n    // check box 展示文本\n    string check_box_show_msg = 10;\n    // 展示文案\n    string text_placeholder = 11;\n    // 弹幕输入框文案\n    string input_placeholder = 12;\n    // 用户举报弹幕 cid维度屏蔽的正则规则\n    repeated string report_filter_content = 13;\n    //\n    ExpoReport expo_report = 14;\n    //\n    BuzzwordConfig buzzword_config = 15;\n    //\n    repeated Expressions expressions = 16;\n    //\n    repeated PostPanel post_panel = 17;\n    //\n    repeated string activity_meta = 18;\n    //\n    repeated PostPanelV2 post_panel2 = 19;\n}\n\n// 客户端弹幕元数据-请求\nmessage DmViewReq {\n    // 稿件avid/漫画epid\n    int64 pid = 1;\n    // 视频cid/漫画cid\n    int64 oid = 2;\n    // 弹幕类型\n    // 1:视频 2:漫画\n    int32 type = 3;\n    // 页面spm\n    string spmid = 4;\n    // 是否冷启\n    int32 is_hard_boot = 5;\n}\n\n// web端弹幕元数据-响应\n// https://api.bilibili.com/x/v2/dm/web/view\nmessage DmWebViewReply {\n    // 是否已关闭弹幕\n    // 0:未关闭 1:已关闭\n    int32 state = 1;\n    //\n    string text = 2;\n    //\n    string text_side = 3;\n    // 分段弹幕配置\n    DmSegConfig dm_sge = 4;\n    // 云屏蔽配置信息\n    DanmakuFlagConfig flag = 5;\n    // 高级弹幕专包url(bfs)\n    repeated string special_dms = 6;\n    // check box 是否展示\n    bool check_box = 7;\n    // 弹幕数\n    int64 count = 8;\n    // 互动弹幕\n    repeated CommandDm commandDms = 9;\n    // 用户弹幕配置\n    DanmuWebPlayerConfig player_config = 10;\n    // 用户举报弹幕 cid维度屏蔽\n    repeated string report_filter_content = 11;\n    //\n    repeated Expressions expressions = 12;\n    //\n    repeated PostPanel post_panel = 13;\n    //\n    repeated string activity_meta = 14;\n}\n\n//\nmessage ExpoReport {\n    //\n    bool should_report_at_end = 1;\n}\n\n//\nenum ExposureType {\n    ExposureTypeNone   = 0; //\n    ExposureTypeDMSend = 1; //\n}\n\n//\nmessage Expression {\n    //\n    repeated string keyword = 1;\n    //\n    string url = 2;\n    //\n    repeated Period period = 3;\n}\n\n//\nmessage Expressions {\n    //\n    repeated Expression data = 1;\n}\n\n// 是否开启弹幕\nmessage InlinePlayerDanmakuSwitch {\n    //\n    bool value = 1;\n} \n\n//\nmessage Label {\n    //\n    string title = 1;\n    //\n    repeated string content = 2;\n}\n\n//\nmessage LabelV2 {\n    //\n    string title = 1;\n    //\n    repeated string content = 2;\n    //\n    bool exposure_once = 3;\n    //\n    int32 exposure_type = 4;\n}\n\n//\nmessage Period {\n    //\n    int64 start = 1;\n    //\n    int64 end = 2;\n}\n\nmessage PlayerDanmakuAiRecommendedLevel   {bool  value = 1;} // 智能云屏蔽等级\nmessage PlayerDanmakuAiRecommendedLevelV2 {int32 value = 1;} //\nmessage PlayerDanmakuAiRecommendedSwitch  {bool  value = 1;} // 是否开启智能云屏蔽\nmessage PlayerDanmakuBlockbottom          {bool  value = 1;} // 是否屏蔽底端弹幕\nmessage PlayerDanmakuBlockcolorful        {bool  value = 1;} // 是否屏蔽彩色弹幕\nmessage PlayerDanmakuBlockrepeat          {bool  value = 1;} // 是否屏蔽重复弹幕\nmessage PlayerDanmakuBlockscroll          {bool  value = 1;} // 是否屏蔽滚动弹幕\nmessage PlayerDanmakuBlockspecial         {bool  value = 1;} // 是否屏蔽高级弹幕\nmessage PlayerDanmakuBlocktop             {bool  value = 1;} // 是否屏蔽顶端弹幕\nmessage PlayerDanmakuDomain               {float value = 1;} // 弹幕显示区域\nmessage PlayerDanmakuEnableblocklist      {bool  value = 1;} // 是否开启屏蔽列表\nmessage PlayerDanmakuOpacity              {float value = 1;} // 弹幕不透明度\nmessage PlayerDanmakuScalingfactor        {float value = 1;} // 弹幕缩放比例\nmessage PlayerDanmakuSeniorModeSwitch     {int32 value = 1;} //\nmessage PlayerDanmakuSpeed                {int32 value = 1;} // 弹幕速度\nmessage PlayerDanmakuSwitch               {bool  value = 1; bool can_ignore = 2;} // 是否开启弹幕\nmessage PlayerDanmakuSwitchSave           {bool  value = 1;} // 是否记录弹幕开关设置\nmessage PlayerDanmakuUseDefaultConfig     {bool  value = 1;} // 是否使用推荐弹幕设置\n\n//\nmessage PostPanel {\n    //\n    int64 start = 1;\n    //\n    int64 end = 2;\n    //\n    int64 priority = 3;\n    //\n    int64 biz_id = 4;\n    //\n    PostPanelBizType biz_type = 5;\n    //\n    ClickButton click_button = 6;\n    //\n    TextInput text_input = 7;\n    //\n    CheckBox check_box = 8;\n    //\n    Toast toast = 9;\n}\n\n//\nenum PostPanelBizType {\n    PostPanelBizTypeNone      = 0; //\n    PostPanelBizTypeEncourage = 1; //\n    PostPanelBizTypeColorDM   = 2; //\n    PostPanelBizTypeNFTDM     = 3; //\n    PostPanelBizTypeFragClose = 4; //\n    PostPanelBizTypeRecommend = 5; //\n}\n\n//\nmessage PostPanelV2 {\n    //\n    int64 start = 1;\n    //\n    int64 end = 2;\n    //\n    int32 biz_type = 3;\n    //\n    ClickButtonV2 click_button = 4;\n    //\n    TextInputV2 text_input = 5;\n    //\n    CheckBoxV2 check_box = 6;\n    //\n    ToastV2 toast = 7;\n    //\n    BubbleV2 bubble = 8;\n    //\n    LabelV2 label = 9;\n    //\n    int32 post_status = 10;\n}\n\n//\nenum PostStatus {\n    PostStatusNormal = 0; //\n    PostStatusClosed = 1; //\n}\n\n//\nenum RenderType {\n    RenderTypeNone     = 0; //\n    RenderTypeSingle   = 1; //\n    RenderTypeRotation = 2; //\n}\n\n// 修改弹幕配置-响应\nmessage Response {\n    //\n    int32 code = 1;\n    //\n    string message = 2;\n}\n\n//\nenum SubtitleAiStatus {\n    None     = 0; //\n    Exposure = 1; //\n    Assist   = 2; //\n}\n\n//\nenum SubtitleAiType {\n    Normal    = 0; //\n    Translate = 1; //\n}\n\n// 单个字幕信息\nmessage SubtitleItem {\n    // 字幕id\n    int64 id = 1;\n    // 字幕id str\n    string id_str = 2;\n    // 字幕语言代码\n    string lan = 3;\n    // 字幕语言\n    string lan_doc = 4;\n    // 字幕文件url\n    string subtitle_url = 5;\n    // 字幕作者信息\n    UserInfo author = 6;\n    // 字幕类型\n    SubtitleType type = 7;\n    //\n    string lan_doc_brief = 8;\n    //\n    SubtitleAiType ai_type = 9;\n    //\n    SubtitleAiStatus ai_status = 10;\n}\n\nenum SubtitleType {\n    CC = 0; // CC字幕\n    AI = 1; // AI生成字幕\n}\n\n//\nmessage TextInput {\n    //\n    repeated string portrait_placeholder = 1;\n    //\n    repeated string landscape_placeholder = 2;\n    //\n    RenderType render_type = 3;\n    //\n    bool placeholder_post = 4;\n    //\n    bool show = 5;\n    //\n    repeated Avatar avatar = 6;\n    //\n    PostStatus post_status = 7;\n    //\n    Label label = 8;\n}\n\n//\nmessage TextInputV2 {\n    //\n    repeated string portrait_placeholder = 1;\n    //\n    repeated string landscape_placeholder = 2;\n    //\n    RenderType render_type = 3;\n    //\n    bool placeholder_post = 4;\n    //\n    repeated Avatar avatar = 5;\n    //\n    int32 text_input_limit = 6;\n}\n\n//\nmessage Toast {\n    //\n    string text = 1;\n    //\n    int32 duration = 2;\n    //\n    bool show = 3;\n    //\n    Button button = 4;\n}\n\n//\nmessage ToastButtonV2 {\n    //\n    string text = 1;\n    //\n    int32 action = 2;\n}\n\n//\nenum ToastFunctionType {\n    ToastFunctionTypeNone      = 0; //\n    ToastFunctionTypePostPanel = 1; //\n}\n\n//\nmessage ToastV2 {\n    //\n    string text = 1;\n    //\n    int32 duration = 2;\n    //\n    ToastButtonV2 toast_button_v2 = 3;\n}\n\n// 字幕作者信息\nmessage UserInfo {\n    // 用户mid\n    int64 mid = 1;\n    // 用户昵称\n    string name = 2;\n    // 用户性别\n    string sex = 3;\n    // 用户头像url\n    string face = 4;\n    // 用户签名\n    string sign = 5;\n    // 用户等级\n    int32 rank = 6;\n}\n\n// 智能防挡弹幕蒙版信息\nmessage VideoMask {\n    // 视频cid\n    int64 cid = 1;\n    // 平台\n    // 0:web端 1:客户端\n    int32 plat = 2;\n    // 帧率\n    int32 fps = 3;\n    // 间隔时间\n    int64 time = 4;\n    // 蒙版url\n    string mask_url = 5;\n}\n\n// 视频字幕信息\nmessage VideoSubtitle {\n    // 视频原语言代码\n    string lan = 1;\n    // 视频原语言\n    string lanDoc = 2;\n    // 视频字幕列表\n    repeated SubtitleItem subtitles = 3;\n}"
  },
  {
    "path": "lib/models/dynamics/result.dart",
    "content": "import 'dart:convert';\n\nclass DynamicsDataModel {\n  DynamicsDataModel({\n    this.hasMore,\n    this.items,\n    this.offset,\n  });\n  bool? hasMore;\n  List<DynamicItemModel>? items;\n  String? offset;\n\n  DynamicsDataModel.fromJson(Map<String, dynamic> json) {\n    hasMore = json['has_more'];\n    items = json['items']\n        .map<DynamicItemModel>((e) => DynamicItemModel.fromJson(e))\n        .toList();\n    offset = json['offset'];\n  }\n}\n\n// 单个动态\nclass DynamicItemModel {\n  DynamicItemModel({\n    this.basic,\n    this.idStr,\n    this.modules,\n    this.orig,\n    this.type,\n    this.visible,\n  });\n\n  Map? basic;\n  String? idStr;\n  ItemModulesModel? modules;\n  ItemOrigModel? orig;\n  String? type;\n  bool? visible;\n\n  DynamicItemModel.fromJson(Map<String, dynamic> json) {\n    basic = json['basic'];\n    idStr = json['id_str'];\n    modules = ItemModulesModel.fromJson(json['modules']);\n    orig = json['orig'] != null ? ItemOrigModel.fromJson(json['orig']) : null;\n    type = json['type'];\n    visible = json['visible'];\n  }\n}\n\nclass ItemOrigModel {\n  ItemOrigModel({\n    this.basic,\n    this.isStr,\n    this.modules,\n    this.type,\n    this.visible,\n  });\n\n  Map? basic;\n  String? isStr;\n  ItemModulesModel? modules;\n  String? type;\n  bool? visible;\n\n  ItemOrigModel.fromJson(Map<String, dynamic> json) {\n    basic = json['basic'];\n    isStr = json['is_str'];\n    modules = ItemModulesModel.fromJson(json['modules']);\n    type = json['type'];\n    visible = json['visible'];\n  }\n}\n\n// 单个动态详情\nclass ItemModulesModel {\n  ItemModulesModel({\n    this.moduleAuthor,\n    this.moduleDynamic,\n    // this.moduleInter,\n    this.moduleStat,\n    this.moduleTag,\n  });\n\n  ModuleAuthorModel? moduleAuthor;\n  ModuleDynamicModel? moduleDynamic;\n  // ModuleInterModel? moduleInter;\n  ModuleStatModel? moduleStat;\n  Map? moduleTag;\n\n  ItemModulesModel.fromJson(Map<String, dynamic> json) {\n    moduleAuthor = json['module_author'] != null\n        ? ModuleAuthorModel.fromJson(json['module_author'])\n        : null;\n    moduleDynamic = json['module_dynamic'] != null\n        ? ModuleDynamicModel.fromJson(json['module_dynamic'])\n        : null;\n    // moduleInter = ModuleInterModel.fromJson(json['module_interaction']);\n    moduleStat = json['module_stat'] != null\n        ? ModuleStatModel.fromJson(json['module_stat'])\n        : null;\n    moduleTag = json['module_tag'];\n  }\n}\n\n// 单个动态详情 - 作者信息\nclass ModuleAuthorModel {\n  ModuleAuthorModel({\n    // this.avatar,\n    // this.decorate,\n    this.face,\n    this.following,\n    this.jumpUrl,\n    this.label,\n    this.mid,\n    this.name,\n    // this.officialVerify,\n    // this.pandant,\n    this.pubAction,\n    // this.pubLocationText,\n    this.pubTime,\n    this.pubTs,\n    this.type,\n    this.vip,\n  });\n\n  String? face;\n  bool? following;\n  String? jumpUrl;\n  String? label;\n  int? mid;\n  String? name;\n  String? pubAction;\n  String? pubTime;\n  int? pubTs;\n  String? type;\n  Map? vip;\n\n  ModuleAuthorModel.fromJson(Map<String, dynamic> json) {\n    face = json['face'];\n    following = json['following'];\n    jumpUrl = json['jump_url'];\n    label = json['label'];\n    mid = json['mid'];\n    name = json['name'];\n    pubAction = json['pub_action'];\n    pubTime = json['pub_time'];\n    pubTs = json['pub_ts'] == 0 ? null : json['pub_ts'];\n    type = json['type'];\n    vip = json['vip'];\n  }\n}\n\n// 单个动态详情 - 动态信息\nclass ModuleDynamicModel {\n  ModuleDynamicModel({\n    this.additional,\n    this.desc,\n    this.major,\n    this.topic,\n  });\n\n  DynamicAddModel? additional;\n  DynamicDescModel? desc;\n  DynamicMajorModel? major;\n  DynamicTopicModel? topic;\n\n  ModuleDynamicModel.fromJson(Map<String, dynamic> json) {\n    additional = json['additional'] != null\n        ? DynamicAddModel.fromJson(json['additional'])\n        : null;\n    desc =\n        json['desc'] != null ? DynamicDescModel.fromJson(json['desc']) : null;\n    if (json['major'] != null) {\n      major = DynamicMajorModel.fromJson(json['major']);\n    }\n    topic = json['topic'] != null\n        ? DynamicTopicModel.fromJson(json['topic'])\n        : null;\n  }\n}\n\n// 单个动态详情 - 评论？信息\n// class ModuleInterModel {\n//   ModuleInterModel({\n\n//   });\n\n//   ModuleInterModel.fromJson(Map<String, dynamic> json) {\n\n//   }\n// }\nclass DynamicAddModel {\n  DynamicAddModel({\n    this.type,\n    this.vote,\n    this.ugc,\n    this.reserve,\n    this.goods,\n  });\n\n  String? type;\n  Vote? vote;\n  Ugc? ugc;\n  Reserve? reserve;\n  Good? goods;\n\n  /// TODO 比赛vs\n  String? match;\n\n  /// TODO 游戏信息\n  String? common;\n\n  DynamicAddModel.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    vote = json['vote'] != null ? Vote.fromJson(json['vote']) : null;\n    ugc = json['ugc'] != null ? Ugc.fromJson(json['ugc']) : null;\n    reserve =\n        json['reserve'] != null ? Reserve.fromJson(json['reserve']) : null;\n    goods = json['goods'] != null ? Good.fromJson(json['goods']) : null;\n  }\n}\n\nclass Vote {\n  Vote({\n    this.choiceCnt,\n    this.defaultShare,\n    this.share,\n    this.endTime,\n    this.joinNum,\n    this.status,\n    this.type,\n    this.uid,\n    this.voteId,\n  });\n\n  int? choiceCnt;\n  String? share;\n  int? defaultShare;\n  int? endTime;\n  int? joinNum;\n  int? status;\n  int? type;\n  int? uid;\n  int? voteId;\n\n  Vote.fromJson(Map<String, dynamic> json) {\n    choiceCnt = json['choice_cnt'];\n    share = json['share'];\n    defaultShare = json['default_share'];\n    endTime = json['end_time'] is int\n        ? json['end_time']\n        : int.parse(json['end_time']);\n    joinNum = json['join_num'];\n    status = json['status'];\n    type = json['type'];\n    uid = json['uid'];\n    voteId = json['vote_id'];\n  }\n}\n\nclass Ugc {\n  Ugc({\n    this.cover,\n    this.descSecond,\n    this.duration,\n    this.headText,\n    this.idStr,\n    this.jumpUrl,\n    this.multiLine,\n    this.title,\n  });\n\n  String? cover;\n  String? descSecond;\n  String? duration;\n  String? headText;\n  String? idStr;\n  String? jumpUrl;\n  bool? multiLine;\n  String? title;\n\n  Ugc.fromJson(Map<String, dynamic> json) {\n    cover = json['cover'];\n    descSecond = json['desc_second'];\n    duration = json['duration'];\n    headText = json['head_text'];\n    idStr = json['id_str'];\n    jumpUrl = json['jump_url'];\n    multiLine = json['multi_line'];\n    title = json['title'];\n  }\n}\n\nclass Reserve {\n  Reserve({\n    this.button,\n    this.desc1,\n    this.desc2,\n    this.jumpUrl,\n    this.reserveTotal,\n    this.rid,\n    this.state,\n    this.stype,\n    this.title,\n    this.upMid,\n  });\n\n  Map? button;\n  Map? desc1;\n  Map? desc2;\n  String? jumpUrl;\n  int? reserveTotal;\n  int? rid;\n  int? state;\n  int? stype;\n  String? title;\n  int? upMid;\n\n  Reserve.fromJson(Map<String, dynamic> json) {\n    button = json['button'];\n    desc1 = json['desc1'];\n    desc2 = json['desc2'];\n    jumpUrl = json['jump_url'];\n    reserveTotal = json['reserve_total'];\n    rid = json['rid'];\n    state = json['state'];\n    state = json['state'];\n    stype = json['stype'];\n    title = json['title'];\n    upMid = json['up_mid'];\n  }\n}\n\nclass Good {\n  Good({\n    this.headIcon,\n    this.headText,\n    this.items,\n    this.jumpUrl,\n  });\n\n  String? headIcon;\n  String? headText;\n  List<GoodItem>? items;\n  String? jumpUrl;\n\n  Good.fromJson(Map<String, dynamic> json) {\n    headIcon = json['head_icon'];\n    headText = json['head_text'];\n    items = json['items'].map<GoodItem>((e) => GoodItem.fromJson(e)).toList();\n    jumpUrl = json['jump_url'];\n  }\n}\n\nclass GoodItem {\n  GoodItem({\n    this.brief,\n    this.cover,\n    this.id,\n    this.jumpDesc,\n    this.jumpUrl,\n    this.name,\n    this.price,\n  });\n\n  String? brief;\n  String? cover;\n  dynamic id;\n  String? jumpDesc;\n  String? jumpUrl;\n  String? name;\n  String? price;\n\n  GoodItem.fromJson(Map<String, dynamic> json) {\n    brief = json['brief'];\n    cover = json['cover'];\n    id = json['id'];\n    jumpDesc = json['jump_desc'];\n    jumpUrl = json['jump_url'];\n    name = json['name'];\n    price = json['price'];\n  }\n}\n\nclass DynamicDescModel {\n  DynamicDescModel({\n    this.richTextNodes,\n    this.text,\n  });\n\n  List<RichTextNodeItem>? richTextNodes;\n  String? text;\n\n  DynamicDescModel.fromJson(Map<String, dynamic> json) {\n    richTextNodes = json['rich_text_nodes'] != null\n        ? json['rich_text_nodes']\n            .map<RichTextNodeItem>((e) => RichTextNodeItem.fromJson(e))\n            .toList()\n        : [];\n    text = json['text'];\n  }\n}\n\n//\nclass DynamicMajorModel {\n  DynamicMajorModel({\n    this.archive,\n    this.draw,\n    this.ugcSeason,\n    this.opus,\n    this.pgc,\n    this.liveRcmd,\n    this.live,\n    this.none,\n    this.type,\n    this.courses,\n    this.common,\n    this.music,\n  });\n\n  DynamicArchiveModel? archive;\n  DynamicDrawModel? draw;\n  DynamicArchiveModel? ugcSeason;\n  DynamicOpusModel? opus;\n  DynamicArchiveModel? pgc;\n  DynamicLiveModel? liveRcmd;\n  DynamicLive2Model? live;\n  DynamicNoneModel? none;\n  // MAJOR_TYPE_DRAW 图片\n  // MAJOR_TYPE_ARCHIVE 视频\n  // MAJOR_TYPE_OPUS 图文/文章\n  String? type;\n  Map? courses;\n  Map? common;\n  Map? music;\n\n  DynamicMajorModel.fromJson(Map<String, dynamic> json) {\n    archive = json['archive'] != null\n        ? DynamicArchiveModel.fromJson(json['archive'])\n        : null;\n    draw =\n        json['draw'] != null ? DynamicDrawModel.fromJson(json['draw']) : null;\n    ugcSeason = json['ugc_season'] != null\n        ? DynamicArchiveModel.fromJson(json['ugc_season'])\n        : null;\n    opus =\n        json['opus'] != null ? DynamicOpusModel.fromJson(json['opus']) : null;\n    pgc =\n        json['pgc'] != null ? DynamicArchiveModel.fromJson(json['pgc']) : null;\n    liveRcmd = json['live_rcmd'] != null\n        ? DynamicLiveModel.fromJson(json['live_rcmd'])\n        : null;\n    live =\n        json['live'] != null ? DynamicLive2Model.fromJson(json['live']) : null;\n    none =\n        json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;\n    type = json['type'];\n    courses = json['courses'] ?? {};\n    common = json['common'] ?? {};\n    music = json['music'] ?? {};\n  }\n}\n\nclass DynamicTopicModel {\n  DynamicTopicModel({\n    this.id,\n    this.jumpUrl,\n    this.name,\n  });\n\n  int? id;\n  String? jumpUrl;\n  String? name;\n\n  DynamicTopicModel.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    jumpUrl = json['jump_url'];\n    name = json['name'];\n  }\n}\n\nclass DynamicArchiveModel {\n  DynamicArchiveModel({\n    this.aid,\n    this.badge,\n    this.bvid,\n    this.cover,\n    this.desc,\n    this.disablePreview,\n    this.durationText,\n    this.jumpUrl,\n    this.stat,\n    this.title,\n    this.type,\n    this.epid,\n    this.seasonId,\n  });\n\n  int? aid;\n  Map? badge;\n  String? bvid;\n  String? cover;\n  String? desc;\n  int? disablePreview;\n  String? durationText;\n  String? jumpUrl;\n  Stat? stat;\n  String? title;\n  int? type;\n  int? epid;\n  int? seasonId;\n\n  DynamicArchiveModel.fromJson(Map<String, dynamic> json) {\n    aid = json['aid'] is String ? int.parse(json['aid']) : json['aid'];\n    badge = json['badge'];\n    bvid = json['bvid'] ?? json['epid'].toString() ?? ' ';\n    cover = json['cover'];\n    disablePreview = json['disable_preview'];\n    durationText = json['duration_text'];\n    jumpUrl = json['jump_url'];\n    stat = json['stat'] != null ? Stat.fromJson(json['stat']) : null;\n    title = json['title'];\n    type = json['type'];\n    epid = json['epid'];\n    seasonId = json['season_id'];\n  }\n}\n\nclass DynamicDrawModel {\n  DynamicDrawModel({\n    this.id,\n    this.items,\n  });\n\n  int? id;\n  List<DynamicDrawItemModel>? items;\n\n  DynamicDrawModel.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    // ignore: prefer_null_aware_operators\n    items = json['items'] != null\n        ? json['items']\n            .map<DynamicDrawItemModel>((e) => DynamicDrawItemModel.fromJson(e))\n            .toList()\n        : null;\n  }\n}\n\nclass DynamicOpusModel {\n  DynamicOpusModel({\n    this.jumpUrl,\n    this.pics,\n    this.summary,\n    this.title,\n  });\n\n  String? jumpUrl;\n  List<OpusPicsModel>? pics;\n  SummaryModel? summary;\n  String? title;\n  DynamicOpusModel.fromJson(Map<String, dynamic> json) {\n    jumpUrl = json['jump_url'];\n    pics = json['pics'] != null\n        ? json['pics']\n            .map<OpusPicsModel>((e) => OpusPicsModel.fromJson(e))\n            .toList()\n        : [];\n    summary =\n        json['summary'] != null ? SummaryModel.fromJson(json['summary']) : null;\n    title = json['title'];\n  }\n}\n\nclass SummaryModel {\n  SummaryModel({\n    this.richTextNodes,\n    this.text,\n  });\n\n  List<RichTextNodeItem>? richTextNodes;\n  String? text;\n\n  SummaryModel.fromJson(Map<String, dynamic> json) {\n    richTextNodes = json['rich_text_nodes']\n        .map<RichTextNodeItem>((e) => RichTextNodeItem.fromJson(e))\n        .toList();\n    text = json['text'];\n  }\n}\n\nclass RichTextNodeItem {\n  RichTextNodeItem({\n    this.emoji,\n    this.origText,\n    this.text,\n    this.type,\n    this.rid,\n  });\n  Emoji? emoji;\n  String? origText;\n  String? text;\n  String? type;\n  String? rid;\n\n  RichTextNodeItem.fromJson(Map<String, dynamic> json) {\n    emoji = json['emoji'] != null ? Emoji.fromJson(json['emoji']) : null;\n    origText = json['orig_text'];\n    text = json['text'];\n    type = json['type'];\n    rid = json['rid'];\n  }\n}\n\nclass Emoji {\n  Emoji({\n    this.iconUrl,\n    this.size,\n    this.text,\n    this.type,\n  });\n\n  String? iconUrl;\n  double? size;\n  String? text;\n  int? type;\n  Emoji.fromJson(Map<String, dynamic> json) {\n    iconUrl = json['icon_url'];\n    size = json['size'].toDouble();\n    text = json['text'];\n    type = json['type'];\n  }\n}\n\nclass DynamicNoneModel {\n  DynamicNoneModel({\n    this.tips,\n  });\n  String? tips;\n  DynamicNoneModel.fromJson(Map<String, dynamic> json) {\n    tips = json['tips'];\n  }\n}\n\nclass OpusPicsModel {\n  OpusPicsModel({\n    this.width,\n    this.height,\n    this.size,\n    this.src,\n    this.url,\n  });\n\n  int? width;\n  int? height;\n  int? size;\n  String? src;\n  String? url;\n\n  OpusPicsModel.fromJson(Map<String, dynamic> json) {\n    width = json['width'];\n    height = json['height'];\n    size = json['size'] != null ? json['size'].toInt() : 0;\n    src = json['src'];\n    url = json['url'];\n  }\n}\n\nclass DynamicDrawItemModel {\n  DynamicDrawItemModel({\n    this.height,\n    this.size,\n    this.src,\n    this.tags,\n    this.width,\n  });\n  int? height;\n  int? size;\n  String? src;\n  List? tags;\n  int? width;\n  DynamicDrawItemModel.fromJson(Map<String, dynamic> json) {\n    height = json['height'];\n    size = json['size'].toInt();\n    src = json['src'];\n    tags = json['tags'];\n    width = json['width'];\n  }\n}\n\nclass DynamicLiveModel {\n  DynamicLiveModel({\n    this.content,\n  });\n\n  String? content;\n  int? type;\n  Map? livePlayInfo;\n  int? uid;\n  String? parentAreaName;\n  int? roomId;\n  String? liveId;\n  int? liveStatus;\n  String? cover;\n  int? online;\n  String? areaName;\n  String? title;\n  int? liveStartTime;\n  Map? watchedShow;\n\n  DynamicLiveModel.fromJson(Map<String, dynamic> json) {\n    content = json['content'];\n    if (json['content'] != null) {\n      Map<String, dynamic> data = jsonDecode(json['content']);\n\n      type = data['type'];\n      Map livePlayInfo = data['live_play_info'];\n      uid = livePlayInfo['uid'];\n      parentAreaName = livePlayInfo['parent_area_name'];\n      roomId = livePlayInfo['room_id'];\n      liveId = livePlayInfo['live_id'];\n      liveStatus = livePlayInfo['live_status'];\n      cover = livePlayInfo['cover'];\n      online = livePlayInfo['online'];\n      areaName = livePlayInfo['area_name'];\n      title = livePlayInfo['title'];\n      liveStartTime = livePlayInfo['live_start_time'];\n      watchedShow = livePlayInfo['watched_show'];\n    }\n  }\n}\n\nclass DynamicLive2Model {\n  DynamicLive2Model({\n    this.badge,\n    this.cover,\n    this.descFirst,\n    this.descSecond,\n    this.id,\n    this.jumpUrl,\n    this.liveState,\n    this.reserveType,\n    this.title,\n  });\n\n  Map? badge;\n  String? cover;\n  String? descFirst;\n  String? descSecond;\n  int? id;\n  String? jumpUrl;\n  int? liveState;\n  int? reserveType;\n  String? title;\n\n  DynamicLive2Model.fromJson(Map<String, dynamic> json) {\n    badge = json['badge'];\n    cover = json['cover'];\n    descFirst = json['desc_first'];\n    descSecond = json['desc_second'];\n    id = json['id'];\n    jumpUrl = json['jump_url'];\n    liveState = json['liv_state'];\n    reserveType = json['reserve_type'];\n    title = json['title'];\n  }\n}\n\n// 动态状态 转发、评论、点赞\nclass ModuleStatModel {\n  ModuleStatModel({\n    this.comment,\n    this.forward,\n    this.like,\n  });\n\n  Comment? comment;\n  ForWard? forward;\n  Like? like;\n\n  ModuleStatModel.fromJson(Map<String, dynamic> json) {\n    comment = Comment.fromJson(json['comment']);\n    forward = ForWard.fromJson(json['forward']);\n    like = Like.fromJson(json['like']);\n  }\n}\n\n// 动态状态 评论\nclass Comment {\n  Comment({\n    this.count,\n    this.forbidden,\n  });\n\n  String? count;\n  bool? forbidden;\n\n  Comment.fromJson(Map<String, dynamic> json) {\n    count = json['count'] == 0 ? null : json['count'].toString();\n    forbidden = json['forbidden'];\n  }\n}\n\nclass ForWard {\n  ForWard({this.count, this.forbidden});\n  String? count;\n  bool? forbidden;\n\n  ForWard.fromJson(Map<String, dynamic> json) {\n    count = json['count'] == 0 ? null : json['count'].toString();\n    forbidden = json['forbidden'];\n  }\n}\n\n// 动态状态 点赞\nclass Like {\n  Like({\n    this.count,\n    this.forbidden,\n    this.status,\n  });\n\n  String? count;\n  bool? forbidden;\n  bool? status;\n\n  Like.fromJson(Map<String, dynamic> json) {\n    count = json['count'] == 0 ? null : json['count'].toString();\n    forbidden = json['forbidden'];\n    status = json['status'];\n  }\n}\n\nclass Stat {\n  Stat({\n    this.danmaku,\n    this.play,\n  });\n\n  String? danmaku;\n  String? play;\n\n  Stat.fromJson(Map<String, dynamic> json) {\n    danmaku = json['danmaku'];\n    play = json['play'];\n  }\n}\n"
  },
  {
    "path": "lib/models/dynamics/up.dart",
    "content": "class FollowUpModel {\n  FollowUpModel({\n    this.liveUsers,\n    this.upList,\n    this.liveList,\n    this.myInfo,\n  });\n\n  LiveUsers? liveUsers;\n  List<UpItem>? upList;\n  List<LiveUserItem>? liveList;\n  MyInfo? myInfo;\n\n  FollowUpModel.fromJson(Map<String, dynamic> json) {\n    liveUsers = json['live_users'] != null\n        ? LiveUsers.fromJson(json['live_users'])\n        : null;\n    liveList = json['live_users'] != null\n        ? json['live_users']['items']\n            .map<LiveUserItem>((e) => LiveUserItem.fromJson(e))\n            .toList()\n        : [];\n    upList = json['up_list'] != null\n        ? json['up_list'].map<UpItem>((e) => UpItem.fromJson(e)).toList()\n        : [];\n    myInfo = json['my_info'] != null ? MyInfo.fromJson(json['my_info']) : null;\n  }\n}\n\nclass LiveUsers {\n  LiveUsers({\n    this.count,\n    this.group,\n    this.items,\n  });\n\n  int? count;\n  String? group;\n  List<LiveUserItem>? items;\n\n  LiveUsers.fromJson(Map<String, dynamic> json) {\n    count = json['count'];\n    group = json['group'];\n    items = json['items']\n        .map<LiveUserItem>((e) => LiveUserItem.fromJson(e))\n        .toList();\n  }\n}\n\nclass LiveUserItem {\n  LiveUserItem({\n    this.face,\n    this.isReserveRecall,\n    this.jumpUrl,\n    this.mid,\n    this.roomId,\n    this.title,\n    this.uname,\n  });\n\n  String? face;\n  bool? isReserveRecall;\n  String? jumpUrl;\n  int? mid;\n  int? roomId;\n  String? title;\n  String? uname;\n  bool hasUpdate = false;\n  String type = 'live';\n\n  LiveUserItem.fromJson(Map<String, dynamic> json) {\n    face = json['face'];\n    isReserveRecall = json['is_reserve_recall'];\n    jumpUrl = json['jump_url'];\n    mid = json['mid'];\n    roomId = json['room_id'];\n    title = json['title'];\n    uname = json['uname'];\n  }\n}\n\nclass UpItem {\n  UpItem({\n    this.face,\n    this.hasUpdate,\n    this.isReserveRecall,\n    this.mid,\n    this.uname,\n  });\n\n  String? face;\n  bool? hasUpdate;\n  bool? isReserveRecall;\n  int? mid;\n  String? uname;\n  String type = 'up';\n\n  UpItem.fromJson(Map<String, dynamic> json) {\n    face = json['face'];\n    hasUpdate = json['has_update'];\n    isReserveRecall = json['is_reserve_recall'];\n    mid = json['mid'];\n    uname = json['uname'];\n  }\n}\n\nclass MyInfo {\n  MyInfo({\n    this.face,\n    this.mid,\n    this.name,\n  });\n\n  String? face;\n  int? mid;\n  String? name;\n\n  MyInfo.fromJson(Map<String, dynamic> json) {\n    face = json['face'];\n    mid = json['mid'];\n    name = json['name'];\n  }\n}\n"
  },
  {
    "path": "lib/models/fans/result.dart",
    "content": "class FansDataModel {\n  FansDataModel({\n    this.total,\n    this.list,\n  });\n\n  int? total;\n  List<FansItemModel>? list;\n\n  FansDataModel.fromJson(Map<String, dynamic> json) {\n    total = json['total'];\n    list = json['list']\n        .map<FansItemModel>((e) => FansItemModel.fromJson(e))\n        .toList();\n  }\n}\n\nclass FansItemModel {\n  FansItemModel({\n    this.mid,\n    this.attribute,\n    this.mtime,\n    this.tag,\n    this.special,\n    this.uname,\n    this.face,\n    this.sign,\n    this.officialVerify,\n  });\n\n  int? mid;\n  int? attribute;\n  int? mtime;\n  List? tag;\n  int? special;\n  String? uname;\n  String? face;\n  String? sign;\n  Map? officialVerify;\n\n  FansItemModel.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    attribute = json['attribute'];\n    mtime = json['mtime'];\n    tag = json['tag'];\n    special = json['special'];\n    uname = json['uname'];\n    face = json['face'];\n    sign = json['sign'] == '' ? '还没有签名' : json['sign'];\n    officialVerify = json['official_verify'];\n  }\n}\n"
  },
  {
    "path": "lib/models/follow/result.dart",
    "content": "class FollowDataModel {\n  FollowDataModel({\n    this.total,\n    this.list,\n  });\n\n  int? total;\n  List<FollowItemModel>? list;\n\n  FollowDataModel.fromJson(Map<String, dynamic> json) {\n    total = json['total'] ?? 0;\n    list = json['list']\n        .map<FollowItemModel>((e) => FollowItemModel.fromJson(e))\n        .toList();\n  }\n}\n\nclass FollowItemModel {\n  FollowItemModel({\n    this.mid,\n    this.attribute,\n    // this.mtime,\n    this.tag,\n    this.special,\n    this.uname,\n    this.face,\n    this.sign,\n    this.officialVerify,\n  });\n\n  int? mid;\n  int? attribute;\n  // int? mtime;\n  List? tag;\n  int? special;\n  String? uname;\n  String? face;\n  String? sign;\n  Map? officialVerify;\n\n  FollowItemModel.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    attribute = json['attribute'];\n    // mtime = json['mtime'];\n    tag = json['tag'];\n    special = json['special'];\n    uname = json['uname'];\n    face = json['face'];\n    sign = json['sign'] == '' ? '还没有签名' : json['sign'];\n    officialVerify = json['official_verify'];\n  }\n}\n"
  },
  {
    "path": "lib/models/github/latest.dart",
    "content": "class LatestDataModel {\n  LatestDataModel({\n    this.url,\n    this.tagName,\n    this.createdAt,\n    this.assets,\n    this.body,\n  });\n\n  String? url;\n  String? tagName;\n  String? createdAt;\n  List? assets;\n  String? body;\n\n  LatestDataModel.fromJson(Map<String, dynamic> json) {\n    url = json['url'];\n    tagName = json['tag_name'];\n    createdAt = json['created_at'];\n    assets = json['assets'] != null\n        ? json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList()\n        : [];\n    body = json['body'];\n  }\n}\n\nclass AssetItem {\n  AssetItem({\n    this.url,\n    this.name,\n    this.size,\n    this.downloadCount,\n    this.downloadUrl,\n  });\n\n  String? url;\n  String? name;\n  int? size;\n  int? downloadCount;\n  String? downloadUrl;\n\n  AssetItem.fromJson(Map<String, dynamic> json) {\n    url = json['url'];\n    name = json['name'];\n    size = json['size'];\n    downloadCount = json['download_count'];\n    downloadUrl = json['browser_download_url'];\n  }\n}\n"
  },
  {
    "path": "lib/models/home/rcmd/result.dart",
    "content": "import 'package:pilipala/utils/id_utils.dart';\n\nclass RecVideoItemAppModel {\n  RecVideoItemAppModel({\n    this.id,\n    this.aid,\n    this.bvid,\n    this.cid,\n    this.pic,\n    this.stat,\n    this.duration,\n    this.title,\n    this.isFollowed,\n    this.owner,\n    this.rcmdReason,\n    this.goto,\n    this.param,\n    this.uri,\n    this.talkBack,\n    this.bangumiView,\n    this.bangumiFollow,\n    this.bangumiBadge,\n    this.cardType,\n    this.adInfo,\n  });\n\n  int? id;\n  int? aid;\n  String? bvid;\n  int? cid;\n  String? pic;\n  RcmdStat? stat;\n  int? duration;\n  String? title;\n  int? isFollowed;\n  RcmdOwner? owner;\n  String? rcmdReason;\n  String? goto;\n  int? param;\n  String? uri;\n  String? talkBack;\n  // 番剧\n  String? bangumiView;\n  String? bangumiFollow;\n  String? bangumiBadge;\n\n  String? cardType;\n  Map? adInfo;\n\n  RecVideoItemAppModel.fromJson(Map<String, dynamic> json) {\n    id = json['player_args'] != null\n        ? json['player_args']['aid']\n        : int.parse(json['param'] ?? '-1');\n    aid = json['player_args'] != null ? json['player_args']['aid'] : -1;\n    bvid = json['player_args'] != null\n        ? IdUtils.av2bv(json['player_args']['aid'])\n        : '';\n    cid = json['player_args'] != null ? json['player_args']['cid'] : -1;\n    pic = json['cover'];\n    stat = RcmdStat.fromJson(json);\n    // 改用player_args中的duration作为原始数据（秒数）\n    duration =\n        json['player_args'] != null ? json['player_args']['duration'] : -1;\n    //duration = json['cover_right_text'];\n    title = json['title'] ?? '获取标题失败';\n    owner = RcmdOwner.fromJson(json);\n    rcmdReason = json['bottom_rcmd_reason'] ?? json['top_rcmd_reason'];\n    // 由于app端api并不会直接返回与owner的关注状态\n    // 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态，从而与web端接口等效\n    RegExp regex = RegExp(r'已关注|新关注');\n    isFollowed = regex.hasMatch(rcmdReason ?? '') ? 1 : 0;\n    // 如果是，就无需再显示推荐原因，交由view统一处理即可\n    if (isFollowed == 1) {\n      rcmdReason = null;\n    }\n    goto = json['goto'];\n    param = int.parse(json['param'] ?? '-1');\n    uri = json['uri'];\n    talkBack = json['talk_back'];\n\n    if (json['goto'] == 'bangumi') {\n      bangumiView = json['cover_left_text_1'];\n      bangumiFollow = json['cover_left_text_2'];\n      bangumiBadge = json['badge'];\n    }\n\n    cardType = json['card_type'];\n    adInfo = json['ad_info'];\n  }\n}\n\nclass RcmdStat {\n  RcmdStat({\n    this.view,\n    this.like,\n    this.danmu,\n  });\n  String? view;\n  String? like;\n  String? danmu;\n\n  RcmdStat.fromJson(Map<String, dynamic> json) {\n    view = json[\"cover_left_text_1\"];\n    danmu = json['cover_left_text_2'] ?? '-';\n  }\n}\n\nclass RcmdOwner {\n  RcmdOwner({this.name, this.mid});\n\n  String? name;\n  int? mid;\n\n  RcmdOwner.fromJson(Map<String, dynamic> json) {\n    name = json['goto'] == 'av'\n        ? json['args']['up_name']\n        : json['desc_button'] != null\n            ? json['desc_button']['text']\n            : '';\n    mid = json['args']['up_id'] ?? -1;\n  }\n}\n\nclass RcmdReason {\n  RcmdReason({\n    this.content,\n  });\n\n  String? content;\n\n  RcmdReason.fromJson(Map<String, dynamic> json) {\n    content = json[\"text\"] ?? '';\n  }\n}\n"
  },
  {
    "path": "lib/models/live/follow.dart",
    "content": "class LiveFollowingModel {\n  int? count;\n  List<LiveFollowingItemModel>? list;\n  int? liveCount;\n  int? neverLivedCount;\n  List? neverLivedFaces;\n  int? pageSize;\n  String? title;\n  int? totalPage;\n\n  LiveFollowingModel({\n    this.count,\n    this.list,\n    this.liveCount,\n    this.neverLivedCount,\n    this.neverLivedFaces,\n    this.pageSize,\n    this.title,\n    this.totalPage,\n  });\n\n  LiveFollowingModel.fromJson(Map<String, dynamic> json) {\n    count = json['count'];\n    if (json['list'] != null) {\n      list = <LiveFollowingItemModel>[];\n      json['list'].forEach((v) {\n        list!.add(LiveFollowingItemModel.fromJson(v));\n      });\n    }\n    liveCount = json['live_count'];\n    neverLivedCount = json['never_lived_count'];\n    if (json['never_lived_faces'] != null) {\n      neverLivedFaces = <dynamic>[];\n      json['never_lived_faces'].forEach((v) {\n        neverLivedFaces!.add(v);\n      });\n    }\n    pageSize = json['pageSize'];\n    title = json['title'];\n    totalPage = json['totalPage'];\n  }\n}\n\nclass LiveFollowingItemModel {\n  int? roomId;\n  int? uid;\n  String? uname;\n  String? title;\n  String? face;\n  int? liveStatus;\n  int? recordNum;\n  String? recentRecordId;\n  int? isAttention;\n  int? clipNum;\n  int? fansNum;\n  String? areaName;\n  String? areaValue;\n  String? tags;\n  String? recentRecordIdV2;\n  int? recordNumV2;\n  int? recordLiveTime;\n  String? areaNameV2;\n  String? roomNews;\n  String? watchIcon;\n  String? textSmall;\n  String? roomCover;\n  String? pic;\n  int? parentAreaId;\n  int? areaId;\n\n  LiveFollowingItemModel({\n    this.roomId,\n    this.uid,\n    this.uname,\n    this.title,\n    this.face,\n    this.liveStatus,\n    this.recordNum,\n    this.recentRecordId,\n    this.isAttention,\n    this.clipNum,\n    this.fansNum,\n    this.areaName,\n    this.areaValue,\n    this.tags,\n    this.recentRecordIdV2,\n    this.recordNumV2,\n    this.recordLiveTime,\n    this.areaNameV2,\n    this.roomNews,\n    this.watchIcon,\n    this.textSmall,\n    this.roomCover,\n    this.pic,\n    this.parentAreaId,\n    this.areaId,\n  });\n\n  LiveFollowingItemModel.fromJson(Map<String, dynamic> json) {\n    roomId = json['roomid'];\n    uid = json['uid'];\n    uname = json['uname'];\n    title = json['title'];\n    face = json['face'];\n    liveStatus = json['live_status'];\n    recordNum = json['record_num'];\n    recentRecordId = json['recent_record_id'];\n    isAttention = json['is_attention'];\n    clipNum = json['clipnum'];\n    fansNum = json['fans_num'];\n    areaName = json['area_name'];\n    areaValue = json['area_value'];\n    tags = json['tags'];\n    recentRecordIdV2 = json['recent_record_id_v2'];\n    recordNumV2 = json['record_num_v2'];\n    recordLiveTime = json['record_live_time'];\n    areaNameV2 = json['area_name_v2'];\n    roomNews = json['room_news'];\n    watchIcon = json['watch_icon'];\n    textSmall = json['text_small'];\n    roomCover = json['room_cover'];\n    pic = json['room_cover'];\n    parentAreaId = json['parent_area_id'];\n    areaId = json['area_id'];\n  }\n}\n"
  },
  {
    "path": "lib/models/live/item.dart",
    "content": "class LiveItemModel {\n  LiveItemModel({\n    this.roomId,\n    this.uid,\n    this.title,\n    this.uname,\n    this.online,\n    this.userCover,\n    this.userCoverFlag,\n    this.systemCover,\n    this.cover,\n    this.pic,\n    this.link,\n    this.face,\n    this.parentId,\n    this.parentName,\n    this.areaId,\n    this.areaName,\n    this.sessionId,\n    this.groupId,\n    this.pkId,\n    this.verify,\n    this.headBox,\n    this.headBoxType,\n    this.watchedShow,\n  });\n\n  int? roomId;\n  int? uid;\n  String? title;\n  String? uname;\n  int? online;\n  String? userCover;\n  int? userCoverFlag;\n  String? systemCover;\n  String? cover;\n  String? pic;\n  String? link;\n  String? face;\n  int? parentId;\n  String? parentName;\n  int? areaId;\n  String? areaName;\n  String? sessionId;\n  int? groupId;\n  int? pkId;\n  Map? verify;\n  Map? headBox;\n  int? headBoxType;\n  Map? watchedShow;\n\n  LiveItemModel.fromJson(Map<String, dynamic> json) {\n    roomId = json['roomid'];\n    uid = json['uid'];\n    title = json['title'];\n    uname = json['uname'];\n    online = json['online'];\n    userCover = json['user_cover'];\n    userCoverFlag = json['user_cover_flag'];\n    systemCover = json['system_cover'];\n    cover = json['cover'];\n    pic = json['cover'];\n    link = json['link'];\n    face = json['face'];\n    parentId = json['parent_id'];\n    parentName = json['parent_name'];\n    areaId = json['area_id'];\n    areaName = json['area_name'];\n    sessionId = json['session_id'];\n    groupId = json['group_id'];\n    pkId = json['pk_id'];\n    verify = json['verify'];\n    headBox = json['head_box'];\n    headBoxType = json['head_box_type'];\n    watchedShow = json['watched_show'];\n  }\n}\n"
  },
  {
    "path": "lib/models/live/message.dart",
    "content": "class LiveMessageModel {\n  // 消息类型\n  final LiveMessageType type;\n\n  // 用户名\n  final String userName;\n\n  // 信息\n  final String? message;\n\n  // 数据\n  final dynamic data;\n\n  final String? face;\n  final int? uid;\n  final Map<String, dynamic>? emots;\n\n  // 颜色\n  final LiveMessageColor color;\n\n  LiveMessageModel({\n    required this.type,\n    required this.userName,\n    required this.message,\n    required this.color,\n    this.data,\n    this.face,\n    this.uid,\n    this.emots,\n  });\n}\n\nclass LiveSuperChatMessage {\n  final String backgroundBottomColor;\n  final String backgroundColor;\n  final DateTime endTime;\n  final String face;\n  final String message;\n  final String price;\n  final DateTime startTime;\n  final String userName;\n\n  LiveSuperChatMessage({\n    required this.backgroundBottomColor,\n    required this.backgroundColor,\n    required this.endTime,\n    required this.face,\n    required this.message,\n    required this.price,\n    required this.startTime,\n    required this.userName,\n  });\n}\n\nenum LiveMessageType {\n  // 普通留言\n  chat,\n  // 醒目留言\n  superChat,\n  //\n  online,\n  // 加入\n  join,\n  // 关注\n  follow,\n}\n\nclass LiveMessageColor {\n  final int r, g, b;\n  LiveMessageColor(this.r, this.g, this.b);\n  static LiveMessageColor get white => LiveMessageColor(255, 255, 255);\n  static LiveMessageColor numberToColor(int intColor) {\n    var obj = intColor.toRadixString(16);\n\n    LiveMessageColor color = LiveMessageColor.white;\n    if (obj.length == 4) {\n      obj = \"00$obj\";\n    }\n    if (obj.length == 6) {\n      var R = int.parse(obj.substring(0, 2), radix: 16);\n      var G = int.parse(obj.substring(2, 4), radix: 16);\n      var B = int.parse(obj.substring(4, 6), radix: 16);\n\n      color = LiveMessageColor(R, G, B);\n    }\n    if (obj.length == 8) {\n      var R = int.parse(obj.substring(2, 4), radix: 16);\n      var G = int.parse(obj.substring(4, 6), radix: 16);\n      var B = int.parse(obj.substring(6, 8), radix: 16);\n      //var A = int.parse(obj.substring(0, 2), radix: 16);\n      color = LiveMessageColor(R, G, B);\n    }\n\n    return color;\n  }\n\n  @override\n  String toString() {\n    return \"#${r.toRadixString(16).padLeft(2, '0')}${g.toRadixString(16).padLeft(2, '0')}${b.toRadixString(16).padLeft(2, '0')}\";\n  }\n}\n"
  },
  {
    "path": "lib/models/live/quality.dart",
    "content": "enum LiveQuality {\n  dolby,\n  super4K,\n  origin,\n  bluRay,\n  superHD,\n  smooth,\n  flunt,\n}\n\nextension LiveQualityCode on LiveQuality {\n  static final List<int> _codeList = [\n    30000,\n    20000,\n    10000,\n    400,\n    250,\n    150,\n    80,\n  ];\n  int get code => _codeList[index];\n\n  static LiveQuality? fromCode(int code) {\n    final index = _codeList.indexOf(code);\n    if (index != -1) {\n      return LiveQuality.values[index];\n    }\n    return null;\n  }\n}\n\nextension VideoQualityDesc on LiveQuality {\n  static final List<String> _descList = [\n    '杜比',\n    '4K',\n    '原画',\n    '蓝光',\n    '超清',\n    '高清',\n    '流畅',\n  ];\n  get description => _descList[index];\n}\n"
  },
  {
    "path": "lib/models/live/room_info.dart",
    "content": "class RoomInfoModel {\n  RoomInfoModel({\n    this.roomId,\n    this.isPortrait,\n    this.liveStatus,\n    this.liveTime,\n    this.playurlInfo,\n  });\n  int? roomId;\n  bool? isPortrait;\n  int? liveStatus;\n  int? liveTime;\n  PlayurlInfo? playurlInfo;\n\n  RoomInfoModel.fromJson(Map<String, dynamic> json) {\n    roomId = json['room_id'];\n    liveStatus = json['live_status'];\n    isPortrait = json['is_portrait'];\n    liveTime = json['live_time'];\n    playurlInfo = PlayurlInfo.fromJson(json['playurl_info']);\n  }\n}\n\nclass PlayurlInfo {\n  PlayurlInfo({\n    this.playurl,\n  });\n\n  Playurl? playurl;\n\n  PlayurlInfo.fromJson(Map<String, dynamic> json) {\n    playurl = Playurl.fromJson(json['playurl']);\n  }\n}\n\nclass Playurl {\n  Playurl({\n    this.cid,\n    this.gQnDesc,\n    this.stream,\n  });\n\n  int? cid;\n  List<GQnDesc>? gQnDesc;\n  List<Streams>? stream;\n\n  Playurl.fromJson(Map<String, dynamic> json) {\n    cid = json['cid'];\n    gQnDesc =\n        json['g_qn_desc'].map<GQnDesc>((e) => GQnDesc.fromJson(e)).toList();\n    stream = json['stream'].map<Streams>((e) => Streams.fromJson(e)).toList();\n  }\n}\n\nclass GQnDesc {\n  GQnDesc({\n    this.qn,\n    this.desc,\n    this.hdrDesc,\n    this.attrDesc,\n  });\n\n  int? qn;\n  String? desc;\n  String? hdrDesc;\n  String? attrDesc;\n\n  GQnDesc.fromJson(Map<String, dynamic> json) {\n    qn = json['qn'];\n    desc = json['desc'];\n    hdrDesc = json['hedr_desc'];\n    attrDesc = json['attr_desc'];\n  }\n}\n\nclass Streams {\n  Streams({\n    this.protocolName,\n    this.format,\n  });\n\n  String? protocolName;\n  List<FormatItem>? format;\n\n  Streams.fromJson(Map<String, dynamic> json) {\n    protocolName = json['protocol_name'];\n    format =\n        json['format'].map<FormatItem>((e) => FormatItem.fromJson(e)).toList();\n  }\n}\n\nclass FormatItem {\n  FormatItem({\n    this.formatName,\n    this.codec,\n  });\n\n  String? formatName;\n  List<CodecItem>? codec;\n\n  FormatItem.fromJson(Map<String, dynamic> json) {\n    formatName = json['format_name'];\n    codec = json['codec'].map<CodecItem>((e) => CodecItem.fromJson(e)).toList();\n  }\n}\n\nclass CodecItem {\n  CodecItem({\n    this.codecName,\n    this.currentQn,\n    this.acceptQn,\n    this.baseUrl,\n    this.urlInfo,\n    this.hdrQn,\n    this.dolbyType,\n    this.attrName,\n  });\n\n  String? codecName;\n  int? currentQn;\n  List? acceptQn;\n  String? baseUrl;\n  List<UrlInfoItem>? urlInfo;\n  String? hdrQn;\n  int? dolbyType;\n  String? attrName;\n\n  CodecItem.fromJson(Map<String, dynamic> json) {\n    codecName = json['codec_name'];\n    currentQn = json['current_qn'];\n    acceptQn = json['accept_qn'];\n    baseUrl = json['base_url'];\n    urlInfo = json['url_info']\n        .map<UrlInfoItem>((e) => UrlInfoItem.fromJson(e))\n        .toList();\n    hdrQn = json['hdr_n'];\n    dolbyType = json['dolby_type'];\n    attrName = json['attr_name'];\n  }\n}\n\nclass UrlInfoItem {\n  UrlInfoItem({\n    this.host,\n    this.extra,\n    this.streamTtl,\n  });\n\n  String? host;\n  String? extra;\n  int? streamTtl;\n\n  UrlInfoItem.fromJson(Map<String, dynamic> json) {\n    host = json['host'];\n    extra = json['extra'];\n    streamTtl = json['stream_ttl'];\n  }\n}\n"
  },
  {
    "path": "lib/models/live/room_info_h5.dart",
    "content": "class RoomInfoH5Model {\n  RoomInfoH5Model({\n    this.roomInfo,\n    this.anchorInfo,\n    this.isRoomFeed,\n    this.watchedShow,\n    this.likeInfoV3,\n    this.blockInfo,\n  });\n\n  RoomInfo? roomInfo;\n  AnchorInfo? anchorInfo;\n  int? isRoomFeed;\n  Map? watchedShow;\n  LikeInfoV3? likeInfoV3;\n  Map? blockInfo;\n\n  RoomInfoH5Model.fromJson(Map<String, dynamic> json) {\n    roomInfo = RoomInfo.fromJson(json['room_info']);\n    anchorInfo = AnchorInfo.fromJson(json['anchor_info']);\n    isRoomFeed = json['is_room_feed'];\n    watchedShow = json['watched_show'];\n    likeInfoV3 = LikeInfoV3.fromJson(json['like_info_v3']);\n    blockInfo = json['block_info'];\n  }\n}\n\nclass RoomInfo {\n  RoomInfo({\n    this.uid,\n    this.roomId,\n    this.title,\n    this.cover,\n    this.description,\n    this.liveStatus,\n    this.liveStartTime,\n    this.areaId,\n    this.areaName,\n    this.parentAreaId,\n    this.parentAreaName,\n    this.online,\n    this.background,\n    this.appBackground,\n    this.liveId,\n  });\n\n  int? uid;\n  int? roomId;\n  String? title;\n  String? cover;\n  String? description;\n  int? liveStatus;\n  int? liveStartTime;\n  int? areaId;\n  String? areaName;\n  int? parentAreaId;\n  String? parentAreaName;\n  int? online;\n  String? background;\n  String? appBackground;\n  String? liveId;\n\n  RoomInfo.fromJson(Map<String, dynamic> json) {\n    uid = json['uid'];\n    roomId = json['room_id'];\n    title = json['title'];\n    cover = json['cover'];\n    description = json['description'];\n    liveStatus = json['liveS_satus'];\n    liveStartTime = json['live_start_time'];\n    areaId = json['area_id'];\n    areaName = json['area_name'];\n    parentAreaId = json['parent_area_id'];\n    parentAreaName = json['parent_area_name'];\n    online = json['online'];\n    background = json['background'];\n    appBackground = json['app_background'];\n    liveId = json['live_id'];\n  }\n}\n\nclass AnchorInfo {\n  AnchorInfo({\n    this.baseInfo,\n    this.relationInfo,\n  });\n\n  BaseInfo? baseInfo;\n  RelationInfo? relationInfo;\n\n  AnchorInfo.fromJson(Map<String, dynamic> json) {\n    baseInfo = BaseInfo.fromJson(json['base_info']);\n    relationInfo = RelationInfo.fromJson(json['relation_info']);\n  }\n}\n\nclass BaseInfo {\n  BaseInfo({\n    this.uname,\n    this.face,\n  });\n\n  String? uname;\n  String? face;\n\n  BaseInfo.fromJson(Map<String, dynamic> json) {\n    uname = json['uname'];\n    face = json['face'];\n  }\n}\n\nclass RelationInfo {\n  RelationInfo({this.attention});\n\n  int? attention;\n\n  RelationInfo.fromJson(Map<String, dynamic> json) {\n    attention = json['attention'];\n  }\n}\n\nclass LikeInfoV3 {\n  LikeInfoV3({this.totalLikes});\n\n  int? totalLikes;\n\n  LikeInfoV3.fromJson(Map<String, dynamic> json) {\n    totalLikes = json['total_likes'];\n  }\n}\n"
  },
  {
    "path": "lib/models/login/index.dart",
    "content": "class CaptchaDataModel {\n  CaptchaDataModel({\n    this.type,\n    this.token,\n    this.geetest,\n    this.tencent,\n    this.validate,\n    this.seccode,\n  });\n\n  String? type;\n  String? token;\n  GeetestData? geetest;\n  Tencent? tencent;\n  String? validate;\n  String? seccode;\n\n  CaptchaDataModel.fromJson(Map<String, dynamic> json) {\n    type = json[\"type\"];\n    token = json[\"token\"];\n    geetest =\n        json[\"geetest\"] != null ? GeetestData.fromJson(json[\"geetest\"]) : null;\n    tencent =\n        json[\"tencent\"] != null ? Tencent.fromJson(json[\"tencent\"]) : null;\n  }\n}\n\nclass GeetestData {\n  GeetestData({\n    this.challenge,\n    this.gt,\n  });\n\n  String? challenge;\n  String? gt;\n\n  GeetestData.fromJson(Map<String, dynamic> json) {\n    challenge = json[\"challenge\"];\n    gt = json[\"gt\"];\n  }\n}\n\nclass Tencent {\n  Tencent({this.appid});\n  String? appid;\n  Tencent.fromJson(Map<String, dynamic> json) {\n    appid = json[\"appid\"];\n  }\n}\n"
  },
  {
    "path": "lib/models/member/archive.dart",
    "content": "class MemberArchiveDataModel {\n  MemberArchiveDataModel({\n    this.list,\n    this.page,\n  });\n\n  ArchiveListModel? list;\n  Map? page;\n\n  MemberArchiveDataModel.fromJson(Map<String, dynamic> json) {\n    list = ArchiveListModel.fromJson(json['list']);\n    page = json['page'];\n  }\n}\n\nclass ArchiveListModel {\n  ArchiveListModel({\n    this.tlist,\n    this.vlist,\n  });\n\n  Map<String, TListItemModel>? tlist;\n  List<VListItemModel>? vlist;\n\n  ArchiveListModel.fromJson(Map<String, dynamic> json) {\n    tlist = json['tlist'] != null\n        ? Map.from(json['tlist']).map((k, v) =>\n            MapEntry<String, TListItemModel>(k, TListItemModel.fromJson(v)))\n        : {};\n    vlist = json['vlist']\n        .map<VListItemModel>((e) => VListItemModel.fromJson(e))\n        .toList();\n  }\n}\n\nclass TListItemModel {\n  TListItemModel({\n    this.tid,\n    this.count,\n    this.name,\n  });\n\n  int? tid;\n  int? count;\n  String? name;\n\n  TListItemModel.fromJson(Map<String, dynamic> json) {\n    tid = json['tid'];\n    count = json['count'];\n    name = json['name'];\n  }\n}\n\nclass VListItemModel {\n  VListItemModel({\n    this.comment,\n    this.typeid,\n    this.play,\n    this.pic,\n    this.subtitle,\n    this.description,\n    this.copyright,\n    this.title,\n    this.review,\n    this.author,\n    this.mid,\n    this.created,\n    this.pubdate,\n    this.length,\n    this.duration,\n    this.videoReview,\n    this.aid,\n    this.bvid,\n    this.cid,\n    this.hideClick,\n    this.isChargingSrc,\n    this.rcmdReason,\n    this.owner,\n  });\n\n  int? comment;\n  int? typeid;\n  int? play;\n  String? pic;\n  String? subtitle;\n  String? description;\n  String? copyright;\n  String? title;\n  int? review;\n  String? author;\n  int? mid;\n  int? created;\n  int? pubdate;\n  String? length;\n  String? duration;\n  int? videoReview;\n  int? aid;\n  String? bvid;\n  int? cid;\n  bool? hideClick;\n  bool? isChargingSrc;\n  Stat? stat;\n  String? rcmdReason;\n  Owner? owner;\n\n  VListItemModel.fromJson(Map<String, dynamic> json) {\n    comment = json['comment'];\n    typeid = json['typeid'];\n    play = json['play'];\n    pic = json['pic'];\n    subtitle = json['subtitle'];\n    description = json['description'];\n    copyright = json['copyright'];\n    title = json['title'];\n    review = json['review'];\n    author = json['author'];\n    mid = json['mid'];\n    created = json['created'];\n    pubdate = json['created'];\n    length = json['length'];\n    duration = json['length'];\n    videoReview = json['video_review'];\n    aid = json['aid'];\n    bvid = json['bvid'];\n    cid = null;\n    hideClick = json['hide_click'];\n    isChargingSrc = json['is_charging_arc'];\n    stat = Stat.fromJson(json);\n    rcmdReason = null;\n    owner = Owner.fromJson(json);\n  }\n}\n\nclass Stat {\n  Stat({\n    this.view,\n    this.danmaku,\n  });\n\n  int? view;\n  int? danmaku;\n\n  Stat.fromJson(Map<String, dynamic> json) {\n    view = json[\"play\"];\n    danmaku = json['video_review'];\n  }\n}\n\nclass Owner {\n  Owner({\n    this.mid,\n    this.name,\n    this.face,\n  });\n  int? mid;\n  String? name;\n  String? face;\n\n  Owner.fromJson(Map<String, dynamic> json) {\n    mid = json[\"mid\"];\n    name = json[\"author\"];\n    face = '';\n  }\n}\n"
  },
  {
    "path": "lib/models/member/article.dart",
    "content": "class MemberArticleDataModel {\n  MemberArticleDataModel({\n    this.hasMore,\n    this.items,\n    this.offset,\n    this.updateNum,\n  });\n\n  bool? hasMore;\n  List<MemberArticleItemModel>? items;\n  String? offset;\n  int? updateNum;\n\n  MemberArticleDataModel.fromJson(Map<String, dynamic> json) {\n    hasMore = json['has_more'];\n    items = json['items']\n        .map<MemberArticleItemModel>((e) => MemberArticleItemModel.fromJson(e))\n        .toList();\n    offset = json['offset'];\n    updateNum = json['update_num'];\n  }\n}\n\nclass MemberArticleItemModel {\n  MemberArticleItemModel({\n    this.content,\n    this.cover,\n    this.jumpUrl,\n    this.opusId,\n    this.stat,\n  });\n\n  String? content;\n  Map? cover;\n  String? jumpUrl;\n  String? opusId;\n  Map? stat;\n\n  MemberArticleItemModel.fromJson(Map<String, dynamic> json) {\n    content = json['content'];\n    cover = json['cover'];\n    jumpUrl = json['jump_url'];\n    opusId = json['opus_id'];\n    stat = json['stat'];\n  }\n}\n"
  },
  {
    "path": "lib/models/member/coin.dart",
    "content": "class MemberCoinsDataModel {\n  MemberCoinsDataModel({\n    this.aid,\n    this.bvid,\n    this.cid,\n    this.coins,\n    this.copyright,\n    this.ctime,\n    this.desc,\n    this.duration,\n    this.owner,\n    this.pic,\n    this.pubLocation,\n    this.pubdate,\n    this.resourceType,\n    this.state,\n    this.subtitle,\n    this.time,\n    this.title,\n    this.tname,\n    this.videos,\n    this.view,\n    this.danmaku,\n  });\n\n  int? aid;\n  String? bvid;\n  int? cid;\n  int? coins;\n  int? copyright;\n  int? ctime;\n  String? desc;\n  int? duration;\n  Owner? owner;\n  String? pic;\n  String? pubLocation;\n  int? pubdate;\n  String? resourceType;\n  int? state;\n  String? subtitle;\n  int? time;\n  String? title;\n  String? tname;\n  int? videos;\n  int? view;\n  int? danmaku;\n\n  MemberCoinsDataModel.fromJson(Map<String, dynamic> json) {\n    aid = json['aid'];\n    bvid = json['bvid'];\n    cid = json['cid'];\n    coins = json['coins'];\n    copyright = json['copyright'];\n    ctime = json['ctime'];\n    desc = json['desc'];\n    duration = json['duration'];\n    owner = Owner.fromJson(json['owner']);\n    pic = json['pic'];\n    pubLocation = json['pub_location'];\n    pubdate = json['pubdate'];\n    resourceType = json['resource_type'];\n    state = json['state'];\n    subtitle = json['subtitle'];\n    time = json['time'];\n    title = json['title'];\n    tname = json['tname'];\n    videos = json['videos'];\n    view = json['stat']['view'];\n    danmaku = json['stat']['danmaku'];\n  }\n}\n\nclass Owner {\n  Owner({\n    this.mid,\n    this.name,\n    this.face,\n  });\n\n  int? mid;\n  String? name;\n  String? face;\n\n  Owner.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    name = json['name'];\n    face = json['face'];\n  }\n}\n"
  },
  {
    "path": "lib/models/member/info.dart",
    "content": "class MemberInfoModel {\n  MemberInfoModel({\n    this.mid,\n    this.name,\n    this.sex,\n    this.face,\n    this.sign,\n    this.level,\n    this.isFollowed,\n    this.topPhoto,\n    this.official,\n    this.vip,\n    this.liveRoom,\n  });\n\n  int? mid;\n  String? name;\n  String? sex;\n  String? face;\n  String? sign;\n  int? level;\n  bool? isFollowed;\n  String? topPhoto;\n  Map? official;\n  Vip? vip;\n  LiveRoom? liveRoom;\n\n  MemberInfoModel.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    name = json['name'];\n    sex = json['sex'];\n    face = json['face'];\n    sign = json['sign'] == '' ? '该用户还没有签名' : json['sign'].replaceAll('\\n', '');\n    level = json['level'];\n    isFollowed = json['is_followed'];\n    topPhoto = json['top_photo'];\n    official = json['official'];\n    vip = Vip.fromJson(json['vip']);\n    liveRoom =\n        json['live_room'] != null ? LiveRoom.fromJson(json['live_room']) : null;\n  }\n}\n\nclass Vip {\n  Vip({\n    this.type,\n    this.status,\n    this.dueDate,\n    this.label,\n    this.nicknameColor,\n  });\n\n  int? type;\n  int? status;\n  int? dueDate;\n  Map? label;\n  int? nicknameColor;\n\n  Vip.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    status = json['status'];\n    dueDate = json['due_date'];\n    label = json['label'];\n    nicknameColor = json['nickname_color'] == ''\n        ? null\n        : int.parse(\"0xFF${json['nickname_color'].replaceAll('#', '')}\");\n  }\n}\n\nclass LiveRoom {\n  LiveRoom({\n    this.roomStatus,\n    this.liveStatus,\n    this.url,\n    this.title,\n    this.cover,\n    this.roomId,\n    this.roundStatus,\n    this.watchedShow,\n  });\n\n  int? roomStatus;\n  int? liveStatus;\n  String? url;\n  String? title;\n  String? cover;\n  int? roomId;\n  int? roundStatus;\n  Map? watchedShow;\n\n  LiveRoom.fromJson(Map<String, dynamic> json) {\n    roomStatus = json['roomStatus'];\n    liveStatus = json['liveStatus'];\n    url = json['url'];\n    title = json['title'];\n    cover = json['cover'];\n    roomId = json['roomid'];\n    roundStatus = json['roundStatus'];\n    watchedShow = json['watched_show'];\n  }\n}\n"
  },
  {
    "path": "lib/models/member/like.dart",
    "content": "class MemberLikeDataModel {\n  MemberLikeDataModel({\n    this.aid,\n    this.videos,\n    this.tid,\n    this.tname,\n    this.pic,\n    this.title,\n    this.pubdate,\n    this.ctime,\n    this.desc,\n    this.state,\n    this.duration,\n    this.redirectUrl,\n    this.rights,\n    this.owner,\n    this.stat,\n    this.dimension,\n    this.cover43,\n    this.bvid,\n    this.interVideo,\n    this.resourceType,\n    this.subtitle,\n    this.enableVt,\n  });\n\n  final int? aid;\n  final int? videos;\n  final int? tid;\n  final String? tname;\n  final String? pic;\n  final String? title;\n  final int? pubdate;\n  final int? ctime;\n  final String? desc;\n  final int? state;\n  final int? duration;\n  final String? redirectUrl;\n  final Rights? rights;\n  final Owner? owner;\n  final Stat? stat;\n  final Dimension? dimension;\n  final String? cover43;\n  final String? bvid;\n  final bool? interVideo;\n  final String? resourceType;\n  final String? subtitle;\n  final int? enableVt;\n\n  factory MemberLikeDataModel.fromJson(Map<String, dynamic> json) =>\n      MemberLikeDataModel(\n        aid: json[\"aid\"],\n        videos: json[\"videos\"],\n        tid: json[\"tid\"],\n        tname: json[\"tname\"],\n        pic: json[\"pic\"],\n        title: json[\"title\"],\n        pubdate: json[\"pubdate\"],\n        ctime: json[\"ctime\"],\n        desc: json[\"desc\"],\n        state: json[\"state\"],\n        duration: json[\"duration\"],\n        redirectUrl: json[\"redirect_url\"],\n        rights: Rights.fromJson(json[\"rights\"]),\n        owner: Owner.fromJson(json[\"owner\"]),\n        stat: Stat.fromJson(json[\"stat\"]),\n        dimension: Dimension.fromJson(json[\"dimension\"]),\n        cover43: json[\"cover43\"],\n        bvid: json[\"bvid\"],\n        interVideo: json[\"inter_video\"],\n        resourceType: json[\"resource_type\"],\n        subtitle: json[\"subtitle\"],\n        enableVt: json[\"enable_vt\"],\n      );\n}\n\nclass Dimension {\n  Dimension({\n    required this.width,\n    required this.height,\n    required this.rotate,\n  });\n\n  final int width;\n  final int height;\n  final int rotate;\n\n  factory Dimension.fromJson(Map<String, dynamic> json) => Dimension(\n        width: json[\"width\"],\n        height: json[\"height\"],\n        rotate: json[\"rotate\"],\n      );\n}\n\nclass Owner {\n  Owner({\n    required this.mid,\n    required this.name,\n    required this.face,\n  });\n\n  final int mid;\n  final String name;\n  final String face;\n\n  factory Owner.fromJson(Map<String, dynamic> json) => Owner(\n        mid: json[\"mid\"],\n        name: json[\"name\"],\n        face: json[\"face\"],\n      );\n}\n\nclass Rights {\n  Rights({\n    required this.bp,\n    required this.elec,\n    required this.download,\n    required this.movie,\n    required this.pay,\n    required this.hd5,\n    required this.noReprint,\n    required this.autoplay,\n    required this.ugcPay,\n    required this.isCooperation,\n    required this.ugcPayPreview,\n    required this.noBackground,\n    required this.arcPay,\n    required this.payFreeWatch,\n  });\n\n  final int bp;\n  final int elec;\n  final int download;\n  final int movie;\n  final int pay;\n  final int hd5;\n  final int noReprint;\n  final int autoplay;\n  final int ugcPay;\n  final int isCooperation;\n  final int ugcPayPreview;\n  final int noBackground;\n  final int arcPay;\n  final int payFreeWatch;\n\n  factory Rights.fromJson(Map<String, dynamic> json) => Rights(\n        bp: json[\"bp\"],\n        elec: json[\"elec\"],\n        download: json[\"download\"],\n        movie: json[\"movie\"],\n        pay: json[\"pay\"],\n        hd5: json[\"hd5\"],\n        noReprint: json[\"no_reprint\"],\n        autoplay: json[\"autoplay\"],\n        ugcPay: json[\"ugc_pay\"],\n        isCooperation: json[\"is_cooperation\"],\n        ugcPayPreview: json[\"ugc_pay_preview\"],\n        noBackground: json[\"no_background\"],\n        arcPay: json[\"arc_pay\"],\n        payFreeWatch: json[\"pay_free_watch\"],\n      );\n}\n\nclass Stat {\n  Stat({\n    required this.aid,\n    required this.view,\n    required this.danmaku,\n    required this.reply,\n    required this.favorite,\n    required this.coin,\n    required this.share,\n    required this.nowRank,\n    required this.hisRank,\n    required this.like,\n    required this.dislike,\n    required this.vt,\n    required this.vv,\n  });\n\n  final int aid;\n  final int view;\n  final int danmaku;\n  final int reply;\n  final int favorite;\n  final int coin;\n  final int share;\n  final int nowRank;\n  final int hisRank;\n  final int like;\n  final int dislike;\n  final int vt;\n  final int vv;\n\n  factory Stat.fromJson(Map<String, dynamic> json) => Stat(\n        aid: json[\"aid\"],\n        view: json[\"view\"],\n        danmaku: json[\"danmaku\"],\n        reply: json[\"reply\"],\n        favorite: json[\"favorite\"],\n        coin: json[\"coin\"],\n        share: json[\"share\"],\n        nowRank: json[\"now_rank\"],\n        hisRank: json[\"his_rank\"],\n        like: json[\"like\"],\n        dislike: json[\"dislike\"],\n        vt: json[\"vt\"],\n        vv: json[\"vv\"],\n      );\n}\n"
  },
  {
    "path": "lib/models/member/seasons.dart",
    "content": "class MemberSeasonsDataModel {\n  MemberSeasonsDataModel({\n    this.page,\n    this.seasonsList,\n    this.seriesList,\n  });\n\n  Map? page;\n  List<MemberSeasonsList>? seasonsList;\n  List<MemberArchiveItem>? seriesList;\n\n  MemberSeasonsDataModel.fromJson(Map<String, dynamic> json) {\n    page = json['page'];\n    var tempList1 = json['seasons_list'] != null\n        ? json['seasons_list']\n            .map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))\n            .toList()\n        : [];\n    var tempList2 = json['series_list'] != null\n        ? json['series_list']\n            .map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))\n            .toList()\n        : [];\n    seriesList = json['archives'] != null\n        ? json['archives']\n            .map<MemberArchiveItem>((e) => MemberArchiveItem.fromJson(e))\n            .toList()\n        : [];\n\n    seasonsList = [...tempList1, ...tempList2];\n  }\n}\n\nclass MemberSeasonsList {\n  MemberSeasonsList({\n    this.archives,\n    this.meta,\n    this.recentAids,\n    this.page,\n  });\n\n  List<MemberArchiveItem>? archives;\n  MamberMeta? meta;\n  List? recentAids;\n  Map? page;\n\n  MemberSeasonsList.fromJson(Map<String, dynamic> json) {\n    archives = json['archives'] != null\n        ? json['archives']\n            .map<MemberArchiveItem>((e) => MemberArchiveItem.fromJson(e))\n            .toList()\n        : [];\n    meta = MamberMeta.fromJson(json['meta']);\n    page = json['page'];\n  }\n}\n\nclass MemberArchiveItem {\n  MemberArchiveItem({\n    this.aid,\n    this.bvid,\n    this.ctime,\n    this.duration,\n    this.pic,\n    this.cover,\n    this.pubdate,\n    this.view,\n    this.title,\n  });\n\n  int? aid;\n  String? bvid;\n  int? ctime;\n  int? duration;\n  String? pic;\n  String? cover;\n  int? pubdate;\n  int? view;\n  String? title;\n\n  MemberArchiveItem.fromJson(Map<String, dynamic> json) {\n    aid = json['aid'];\n    bvid = json['bvid'];\n    ctime = json['ctime'];\n    duration = json['duration'];\n    pic = json['pic'];\n    cover = json['pic'];\n    pubdate = json['pubdate'];\n    view = json['stat']['view'];\n    title = json['title'];\n  }\n}\n\nclass MamberMeta {\n  MamberMeta({\n    this.cover,\n    this.description,\n    this.mid,\n    this.name,\n    this.ptime,\n    this.seasonId,\n    this.total,\n    this.seriesId,\n    this.category,\n  });\n\n  String? cover;\n  String? description;\n  int? mid;\n  String? name;\n  int? ptime;\n  int? seasonId;\n  int? total;\n  int? seriesId;\n  int? category;\n\n  MamberMeta.fromJson(Map<String, dynamic> json) {\n    cover = json['cover'];\n    description = json['description'];\n    mid = json['mid'];\n    name = json['name'];\n    ptime = json['ptime'];\n    seasonId = json['season_id'];\n    total = json['total'];\n    seriesId = json['series_id'];\n    category = json['category'];\n  }\n}\n"
  },
  {
    "path": "lib/models/member/tags.dart",
    "content": "class MemberTagItemModel {\n  MemberTagItemModel({\n    this.count,\n    this.name,\n    this.tagid,\n    this.tip,\n    this.checked,\n  });\n\n  int? count;\n  String? name;\n  int? tagid;\n  String? tip;\n  bool? checked;\n\n  MemberTagItemModel.fromJson(Map<String, dynamic> json) {\n    count = json['count'];\n    name = json['name'];\n    tagid = json['tagid'];\n    tip = json['tip'];\n    checked = false;\n  }\n}\n"
  },
  {
    "path": "lib/models/model_hot_video_item.dart",
    "content": "import './model_owner.dart';\n\nclass HotVideoItemModel {\n  HotVideoItemModel({\n    this.aid,\n    this.cid,\n    this.bvid,\n    this.videos,\n    this.tid,\n    this.tname,\n    this.copyright,\n    this.pic,\n    this.title,\n    this.pubdate,\n    this.ctime,\n    this.desc,\n    this.state,\n    this.duration,\n    this.middionId,\n    this.owner,\n    this.stat,\n    this.vDynamic,\n    this.dimension,\n    this.shortLinkV2,\n    this.firstFrame,\n    this.cover,\n    this.pubLocation,\n    this.seasontype,\n    this.isOgv,\n    this.rcmdReason,\n  });\n\n  int? aid;\n  int? cid;\n  String? bvid;\n  int? videos;\n  int? tid;\n  String? tname;\n  int? copyright;\n  String? pic;\n  String? title;\n  int? pubdate;\n  int? ctime;\n  String? desc;\n  int? state;\n  int? duration;\n  int? middionId;\n  Owner? owner;\n  Stat? stat;\n  String? vDynamic;\n  Dimension? dimension;\n  String? shortLinkV2;\n  String? firstFrame;\n  String? cover;\n  String? pubLocation;\n  int? seasontype;\n  bool? isOgv;\n  RcmdReason? rcmdReason;\n\n  HotVideoItemModel.fromJson(Map<String, dynamic> json) {\n    aid = json[\"aid\"];\n    cid = json[\"cid\"];\n    bvid = json[\"bvid\"];\n    videos = json[\"videos\"];\n    tid = json[\"tid\"];\n    tname = json[\"tname\"];\n    copyright = json[\"copyright\"];\n    pic = json[\"pic\"];\n    title = json[\"title\"];\n    pubdate = json[\"pubdate\"];\n    ctime = json[\"ctime\"];\n    desc = json[\"desc\"];\n    state = json[\"state\"];\n    duration = json[\"duration\"];\n    middionId = json[\"middion_id\"];\n    owner = Owner.fromJson(json[\"owner\"]);\n    stat = Stat.fromJson(json['stat']);\n    vDynamic = json[\"vDynamic\"];\n    dimension = Dimension.fromMap(json['dimension']);\n    shortLinkV2 = json[\"short_link_v2\"];\n    firstFrame = json[\"first_frame\"];\n    cover = json[\"first_frame\"];\n    pubLocation = json[\"pub_location\"];\n    seasontype = json[\"seasontype\"];\n    isOgv = json[\"isOgv\"];\n    rcmdReason = json['rcmd_reason'] != '' && json['rcmd_reason'] != null\n        ? RcmdReason.fromJson(json['rcmd_reason'])\n        : null;\n  }\n}\n\nclass Stat {\n  Stat({\n    this.aid,\n    this.view,\n    this.danmaku,\n    this.reply,\n    this.favorite,\n    this.coin,\n    this.share,\n    this.nowRank,\n    this.hisRank,\n    this.like,\n    this.dislike,\n    this.vt,\n    this.vv,\n  });\n\n  int? aid;\n  int? view;\n  int? danmaku;\n  int? reply;\n  int? favorite;\n  int? coin;\n  int? share;\n  int? nowRank;\n  int? hisRank;\n  int? like;\n  int? dislike;\n  int? vt;\n  int? vv;\n\n  Stat.fromJson(Map<String, dynamic> json) {\n    aid = json[\"aid\"];\n    view = json[\"view\"];\n    danmaku = json['danmaku'];\n    reply = json[\"reply\"];\n    favorite = json[\"favorite\"];\n    coin = json['coin'];\n    share = json[\"share\"];\n    nowRank = json[\"now_rank\"];\n    hisRank = json['his_rank'];\n    like = json[\"like\"];\n    dislike = json[\"dislike\"];\n    vt = json['vt'];\n    vv = json[\"vv\"];\n  }\n}\n\nclass Dimension {\n  Dimension({this.width, this.height, this.rotate});\n\n  int? width;\n  int? height;\n  int? rotate;\n\n  Dimension.fromMap(Map<String, dynamic> json) {\n    width = json[\"width\"];\n    height = json[\"height\"];\n    rotate = json[\"rotate\"];\n  }\n}\n\nclass RcmdReason {\n  RcmdReason({\n    this.rcornerMark,\n    this.content,\n  });\n\n  int? rcornerMark;\n  String? content = '';\n\n  RcmdReason.fromJson(Map<String, dynamic> json) {\n    rcornerMark = json[\"corner_mark\"];\n    content = json[\"content\"] ?? '';\n  }\n}\n"
  },
  {
    "path": "lib/models/model_owner.dart",
    "content": "class Owner {\n  Owner({\n    this.mid,\n    this.name,\n    this.face,\n  });\n\n  int? mid;\n  String? name;\n  String? face;\n\n  Owner.fromJson(Map<String, dynamic> json) {\n    mid = json[\"mid\"];\n    name = json[\"name\"];\n    face = json['face'];\n  }\n}\n"
  },
  {
    "path": "lib/models/model_rec_video_item.dart",
    "content": "import './model_owner.dart';\n\nclass RecVideoItemModel {\n  RecVideoItemModel({\n    this.id,\n    this.bvid,\n    this.cid,\n    this.goto,\n    this.uri,\n    this.pic,\n    this.title,\n    this.duration,\n    this.pubdate,\n    this.owner,\n    this.stat,\n    this.isFollowed,\n    this.rcmdReason,\n  });\n\n  int? id = -1;\n  String? bvid = '';\n  int? cid = -1;\n  String? goto = '';\n  String? uri = '';\n  String? pic = '';\n  String? title = '';\n  int? duration = -1;\n  int? pubdate = -1;\n  Owner? owner;\n  Stat? stat;\n  int? isFollowed;\n  String? rcmdReason;\n\n  RecVideoItemModel.fromJson(Map<String, dynamic> json) {\n    id = json[\"id\"];\n    bvid = json[\"bvid\"];\n    cid = json[\"cid\"];\n    goto = json[\"goto\"];\n    uri = json[\"uri\"];\n    pic = json[\"pic\"];\n    title = json[\"title\"];\n    duration = json[\"duration\"];\n    pubdate = json[\"pubdate\"];\n    owner = Owner.fromJson(json[\"owner\"]);\n    stat = Stat.fromJson(json[\"stat\"]);\n    isFollowed = json[\"is_followed\"] ?? 0;\n    rcmdReason = json[\"rcmd_reason\"]?['content'];\n  }\n}\n\nclass Stat {\n  Stat({\n    this.view,\n    this.like,\n    this.danmu,\n  });\n\n  int? view;\n  int? like;\n  int? danmu;\n  Stat.fromJson(Map<String, dynamic> json) {\n    // 无需在model中转换以保留原始数据，在view层处理即可\n    view = json[\"view\"];\n    like = json[\"like\"];\n    danmu = json['danmaku'];\n  }\n}\n"
  },
  {
    "path": "lib/models/msg/account.dart",
    "content": "class AccountListModel {\n  AccountListModel({\n    this.mid,\n    this.name,\n    this.sex,\n    this.face,\n    this.sign,\n    this.rank,\n    this.level,\n    this.silence,\n    this.vip,\n    this.pendant,\n    this.nameplate,\n    this.official,\n    this.birthday,\n    this.isFakeAccount,\n    this.isDeleted,\n    this.inRegAudit,\n    this.faceNft,\n    this.faceNftNew,\n    this.isSeniorMember,\n    this.digitalId,\n    this.digitalType,\n    this.attestation,\n    this.expertInfo,\n    this.honours,\n  });\n\n  int? mid;\n  String? name;\n  String? sex;\n  String? face;\n  String? sign;\n  int? rank;\n  int? level;\n  int? silence;\n  Map? vip;\n  Map? pendant;\n  Map? nameplate;\n  Map? official;\n  int? birthday;\n  int? isFakeAccount;\n  int? isDeleted;\n  int? inRegAudit;\n  int? faceNft;\n  int? faceNftNew;\n  int? isSeniorMember;\n  String? digitalId;\n  int? digitalType;\n  Map? attestation;\n  Map? expertInfo;\n  Map? honours;\n\n  AccountListModel.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    name = json['name'] ?? '';\n    sex = json['sex'];\n    face = json['face'];\n    sign = json['sign'];\n    rank = json['rank'];\n    level = json['level'];\n    silence = json['silence'];\n    vip = json['vip'];\n    pendant = json['pendant'];\n    nameplate = json['nameplate'];\n    official = json['official'];\n    birthday = json['birthday'];\n    isFakeAccount = json['is_fake_account'];\n    isDeleted = json['is_deleted'];\n    inRegAudit = json['in_reg_audit'];\n    faceNft = json['face_nft'];\n    faceNftNew = json['face_nft_new'];\n    isSeniorMember = json['is_senior_member'];\n    digitalId = json['digital_id'];\n    digitalType = json['digital_type'];\n    attestation = json['attestation'];\n    expertInfo = json['expert_info'];\n    honours = json['honours'];\n  }\n}\n"
  },
  {
    "path": "lib/models/msg/like.dart",
    "content": "class MessageLikeModel {\n  MessageLikeModel({\n    this.latest,\n    this.total,\n  });\n\n  Latest? latest;\n  Total? total;\n\n  factory MessageLikeModel.fromJson(Map<String, dynamic> json) =>\n      MessageLikeModel(\n        latest: json[\"latest\"] == null ? null : Latest.fromJson(json[\"latest\"]),\n        total: json[\"total\"] == null ? null : Total.fromJson(json[\"total\"]),\n      );\n}\n\nclass Latest {\n  Latest({\n    this.items,\n    this.lastViewAt,\n  });\n\n  List? items;\n  int? lastViewAt;\n\n  factory Latest.fromJson(Map<String, dynamic> json) => Latest(\n        items: json[\"items\"],\n        lastViewAt: json[\"last_view_at\"],\n      );\n}\n\nclass Total {\n  Total({\n    this.cursor,\n    this.items,\n  });\n\n  Cursor? cursor;\n  List<MessageLikeItem>? items;\n\n  factory Total.fromJson(Map<String, dynamic> json) => Total(\n        cursor: Cursor.fromJson(json['cursor']),\n        items: json[\"items\"] == null\n            ? []\n            : json[\"items\"].map<MessageLikeItem>((e) {\n                return MessageLikeItem.fromJson(e);\n              }).toList(),\n      );\n}\n\nclass Cursor {\n  Cursor({\n    this.id,\n    this.isEnd,\n    this.time,\n  });\n\n  int? id;\n  bool? isEnd;\n  int? time;\n\n  factory Cursor.fromJson(Map<String, dynamic> json) => Cursor(\n        id: json['id'],\n        isEnd: json['is_end'],\n        time: json['time'],\n      );\n}\n\nclass MessageLikeItem {\n  MessageLikeItem({\n    this.id,\n    this.users,\n    this.item,\n    this.counts,\n    this.likeTime,\n    this.noticeState,\n    this.isExpand = false,\n  });\n\n  int? id;\n  List<MessageLikeUser>? users;\n  MessageLikeItemItem? item;\n  int? counts;\n  int? likeTime;\n  int? noticeState;\n  bool isExpand;\n\n  factory MessageLikeItem.fromJson(Map<String, dynamic> json) =>\n      MessageLikeItem(\n        id: json[\"id\"],\n        users: json[\"users\"] == null\n            ? []\n            : json[\"users\"].map<MessageLikeUser>((e) {\n                return MessageLikeUser.fromJson(e);\n              }).toList(),\n        item: json[\"item\"] == null\n            ? null\n            : MessageLikeItemItem.fromJson(json[\"item\"]),\n        counts: json[\"counts\"],\n        likeTime: json[\"like_time\"],\n        noticeState: json[\"notice_state\"],\n      );\n}\n\nclass MessageLikeUser {\n  MessageLikeUser({\n    this.mid,\n    this.fans,\n    this.nickname,\n    this.avatar,\n    this.midLink,\n    this.follow,\n  });\n\n  int? mid;\n  int? fans;\n  String? nickname;\n  String? avatar;\n  String? midLink;\n  bool? follow;\n\n  factory MessageLikeUser.fromJson(Map<String, dynamic> json) =>\n      MessageLikeUser(\n        mid: json[\"mid\"],\n        fans: json[\"fans\"],\n        nickname: json[\"nickname\"],\n        avatar: json[\"avatar\"],\n        midLink: json[\"mid_link\"],\n        follow: json[\"follow\"],\n      );\n}\n\nclass MessageLikeItemItem {\n  MessageLikeItemItem({\n    this.itemId,\n    this.pid,\n    this.type,\n    this.business,\n    this.businessId,\n    this.replyBusinessId,\n    this.likeBusinessId,\n    this.title,\n    this.desc,\n    this.image,\n    this.uri,\n    this.detailName,\n    this.nativeUri,\n    this.ctime,\n  });\n\n  int? itemId;\n  int? pid;\n  String? type;\n  String? business;\n  int? businessId;\n  int? replyBusinessId;\n  int? likeBusinessId;\n  String? title;\n  String? desc;\n  String? image;\n  String? uri;\n  String? detailName;\n  String? nativeUri;\n  int? ctime;\n\n  factory MessageLikeItemItem.fromJson(Map<String, dynamic> json) =>\n      MessageLikeItemItem(\n        itemId: json[\"item_id\"],\n        pid: json[\"pid\"],\n        type: json[\"type\"],\n        business: json[\"business\"],\n        businessId: json[\"business_id\"],\n        replyBusinessId: json[\"reply_business_id\"],\n        likeBusinessId: json[\"like_business_id\"],\n        title: json[\"title\"],\n        desc: json[\"desc\"],\n        image: json[\"image\"],\n        uri: json[\"uri\"],\n        detailName: json[\"detail_name\"],\n        nativeUri: json[\"native_uri\"],\n        ctime: json[\"ctime\"],\n      );\n}\n"
  },
  {
    "path": "lib/models/msg/reply.dart",
    "content": "class MessageReplyModel {\n  MessageReplyModel({\n    this.cursor,\n    this.items,\n  });\n\n  Cursor? cursor;\n  List<MessageReplyItem>? items;\n\n  MessageReplyModel.fromJson(Map<String, dynamic> json) {\n    cursor = Cursor.fromJson(json['cursor']);\n    items = json[\"items\"] != null\n        ? json[\"items\"].map<MessageReplyItem>((e) {\n            return MessageReplyItem.fromJson(e);\n          }).toList()\n        : [];\n  }\n}\n\nclass Cursor {\n  Cursor({\n    this.id,\n    this.isEnd,\n    this.time,\n  });\n\n  int? id;\n  bool? isEnd;\n  int? time;\n\n  Cursor.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    isEnd = json['is_end'];\n    time = json['time'];\n  }\n}\n\nclass MessageReplyItem {\n  MessageReplyItem({\n    this.count,\n    this.id,\n    this.isMulti,\n    this.item,\n    this.replyTime,\n    this.user,\n  });\n\n  int? count;\n  int? id;\n  int? isMulti;\n  ReplyContentItem? item;\n  int? replyTime;\n  ReplyUser? user;\n\n  MessageReplyItem.fromJson(Map<String, dynamic> json) {\n    count = json['count'];\n    id = json['id'];\n    isMulti = json['is_multi'];\n    item = ReplyContentItem.fromJson(json[\"item\"]);\n    replyTime = json['reply_time'];\n    user = ReplyUser.fromJson(json['user']);\n  }\n}\n\nclass ReplyContentItem {\n  ReplyContentItem({\n    this.subjectId,\n    this.rootId,\n    this.sourceId,\n    this.targetId,\n    this.type,\n    this.businessId,\n    this.business,\n    this.title,\n    this.desc,\n    this.image,\n    this.uri,\n    this.nativeUri,\n    this.detailTitle,\n    this.rootReplyContent,\n    this.sourceContent,\n    this.targetReplyContent,\n    this.atDetails,\n    this.topicDetails,\n    this.hideReplyButton,\n    this.hideLikeButton,\n    this.likeState,\n    this.danmu,\n    this.message,\n  });\n\n  int? subjectId;\n  int? rootId;\n  int? sourceId;\n  int? targetId;\n  String? type;\n  int? businessId;\n  String? business;\n  String? title;\n  String? desc;\n  String? image;\n  String? uri;\n  String? nativeUri;\n  String? detailTitle;\n  String? rootReplyContent;\n  String? sourceContent;\n  String? targetReplyContent;\n  List? atDetails;\n  List? topicDetails;\n  bool? hideReplyButton;\n  bool? hideLikeButton;\n  int? likeState;\n  String? danmu;\n  String? message;\n\n  ReplyContentItem.fromJson(Map<String, dynamic> json) {\n    subjectId = json['subject_id'];\n    rootId = json['root_id'];\n    sourceId = json['source_id'];\n    targetId = json['target_id'];\n    type = json['type'];\n    businessId = json['business_id'];\n    business = json['business'];\n    title = json['title'];\n    desc = json['desc'];\n    image = json['image'];\n    uri = json['uri'];\n    nativeUri = json['native_uri'];\n    detailTitle = json['detail_title'];\n    rootReplyContent = json['root_reply_content'];\n    sourceContent = json['source_content'];\n    targetReplyContent = json['target_reply_content'];\n    atDetails = json['at_details'];\n    topicDetails = json['topic_details'];\n    hideReplyButton = json['hide_reply_button'];\n    hideLikeButton = json['hide_like_button'];\n    likeState = json['like_state'];\n    danmu = json['danmu'];\n    message = json['message'];\n  }\n}\n\nclass ReplyUser {\n  ReplyUser({\n    this.mid,\n    this.fans,\n    this.nickname,\n    this.avatar,\n    this.midLink,\n    this.follow,\n  });\n\n  int? mid;\n  int? fans;\n  String? nickname;\n  String? avatar;\n  String? midLink;\n  bool? follow;\n\n  ReplyUser.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    fans = json['fans'];\n    nickname = json['nickname'];\n    avatar = json['avatar'];\n    midLink = json['mid_link'];\n    follow = json['follow'];\n  }\n}\n"
  },
  {
    "path": "lib/models/msg/session.dart",
    "content": "import 'dart:convert';\n\nimport 'package:pilipala/models/msg/account.dart';\n\nclass SessionDataModel {\n  SessionDataModel({\n    this.sessionList,\n    this.hasMore,\n  });\n\n  List<SessionList>? sessionList;\n  int? hasMore;\n\n  SessionDataModel.fromJson(Map<String, dynamic> json) {\n    sessionList = json['session_list']\n        ?.map<SessionList>((e) => SessionList.fromJson(e))\n        .toList();\n    hasMore = json['has_more'];\n  }\n}\n\nclass SessionList {\n  SessionList({\n    this.talkerId,\n    this.sessionType,\n    this.atSeqno,\n    this.topTs,\n    this.groupName,\n    this.groupCover,\n    this.isFollow,\n    this.isDnd,\n    this.ackSeqno,\n    this.ackTs,\n    this.sessionTs,\n    this.unreadCount,\n    this.lastMsg,\n    this.groupType,\n    this.canFold,\n    this.status,\n    this.maxSeqno,\n    this.newPushMsg,\n    this.setting,\n    this.isGuardian,\n    this.isIntercept,\n    this.isTrust,\n    this.systemMsgType,\n    this.liveStatus,\n    this.bizMsgUnreadCount,\n    // this.userLabel,\n  });\n\n  int? talkerId;\n  int? sessionType;\n  int? atSeqno;\n  int? topTs;\n  String? groupName;\n  String? groupCover;\n  int? isFollow;\n  int? isDnd;\n  int? ackSeqno;\n  int? ackTs;\n  int? sessionTs;\n  int? unreadCount;\n  LastMsg? lastMsg;\n  int? groupType;\n  int? canFold;\n  int? status;\n  int? maxSeqno;\n  int? newPushMsg;\n  int? setting;\n  int? isGuardian;\n  int? isIntercept;\n  int? isTrust;\n  int? systemMsgType;\n  int? liveStatus;\n  int? bizMsgUnreadCount;\n  // int? userLabel;\n  AccountListModel? accountInfo;\n\n  SessionList.fromJson(Map<String, dynamic> json) {\n    talkerId = json[\"talker_id\"];\n    sessionType = json[\"session_type\"];\n    atSeqno = json[\"at_seqno\"];\n    topTs = json[\"top_ts\"];\n    groupName = json[\"group_name\"];\n    groupCover = json[\"group_cover\"];\n    isFollow = json[\"is_follow\"];\n    isDnd = json[\"is_dnd\"];\n    ackSeqno = json[\"ack_seqno\"];\n    ackTs = json[\"ack_ts\"];\n    sessionTs = json[\"session_ts\"];\n    unreadCount = json[\"unread_count\"];\n    lastMsg =\n        json[\"last_msg\"] != null ? LastMsg.fromJson(json[\"last_msg\"]) : null;\n    groupType = json[\"group_type\"];\n    canFold = json[\"can_fold\"];\n    status = json[\"status\"];\n    maxSeqno = json[\"max_seqno\"];\n    newPushMsg = json[\"new_push_msg\"];\n    setting = json[\"setting\"];\n    isGuardian = json[\"is_guardian\"];\n    isIntercept = json[\"is_intercept\"];\n    isTrust = json[\"is_trust\"];\n    systemMsgType = json[\"system_msg_type\"];\n    liveStatus = json[\"live_status\"];\n    bizMsgUnreadCount = json[\"biz_msg_unread_count\"];\n    // userLabel = json[\"user_label\"];\n  }\n}\n\nclass LastMsg {\n  LastMsg({\n    this.senderIid,\n    this.receiverType,\n    this.receiverId,\n    this.msgType,\n    this.content,\n    this.msgSeqno,\n    this.timestamp,\n    this.atUids,\n    this.msgKey,\n    this.msgStatus,\n    this.notifyCode,\n    // this.newFaceVersion,\n  });\n\n  int? senderIid;\n  int? receiverType;\n  int? receiverId;\n  int? msgType;\n  dynamic content;\n  int? msgSeqno;\n  int? timestamp;\n  String? atUids;\n  int? msgKey;\n  int? msgStatus;\n  String? notifyCode;\n  // int? newFaceVersion;\n\n  LastMsg.fromJson(Map<String, dynamic> json) {\n    senderIid = json['sender_uid'];\n    receiverType = json['receiver_type'];\n    receiverId = json['receiver_id'];\n    msgType = json['msg_type'];\n    content = json['content'] != null && json['content'] != ''\n        ? jsonDecode(json['content'])\n        : '';\n    msgSeqno = json['msg_seqno'];\n    timestamp = json['timestamp'];\n    atUids = json['at_uids'];\n    msgKey = json['msg_key'];\n    msgStatus = json['msg_status'];\n    notifyCode = json['notify_code'];\n    // newFaceVersion = json['new_face_version'];\n  }\n}\n\nclass SessionMsgDataModel {\n  SessionMsgDataModel({\n    this.messages,\n    this.hasMore,\n    this.minSeqno,\n    this.maxSeqno,\n    this.eInfos,\n  });\n\n  List<MessageItem>? messages;\n  int? hasMore;\n  int? minSeqno;\n  int? maxSeqno;\n  List<dynamic>? eInfos;\n\n  SessionMsgDataModel.fromJson(Map<String, dynamic> json) {\n    messages = json['messages']\n        .map<MessageItem>((e) => MessageItem.fromJson(e))\n        .toList();\n    hasMore = json['has_more'];\n    minSeqno = json['min_seqno'];\n    maxSeqno = json['max_seqno'];\n    eInfos = json['e_infos'];\n  }\n}\n\nclass MessageItem {\n  MessageItem({\n    this.senderUid,\n    this.receiverType,\n    this.receiverId,\n    this.msgType,\n    this.content,\n    this.msgSeqno,\n    this.timestamp,\n    this.atUids,\n    this.msgKey,\n    this.msgStatus,\n    this.notifyCode,\n    this.newFaceVersion,\n  });\n\n  int? senderUid;\n  int? receiverType;\n  int? receiverId;\n  int? msgType;\n  dynamic content;\n  int? msgSeqno;\n  int? timestamp;\n  List? atUids;\n  int? msgKey;\n  int? msgStatus;\n  String? notifyCode;\n  int? newFaceVersion;\n\n  MessageItem.fromJson(Map<String, dynamic> json) {\n    senderUid = json['sender_uid'];\n    receiverType = json['receiver_type'];\n    receiverId = json['receiver_id'];\n    // 1 文本 2 图片 18 系统提示 10 系统通知  5 撤回的消息\n    msgType = json['msg_type'];\n    content = json['content'] != null && json['content'] != ''\n        ? jsonDecode(json['content'])\n        : '';\n    msgSeqno = json['msg_seqno'];\n    timestamp = json['timestamp'];\n    atUids = json['at_uids'];\n    msgKey = json['msg_key'];\n    msgStatus = json['msg_status'];\n    notifyCode = json['notify_code'];\n    newFaceVersion = json['new_face_version'];\n  }\n}\n"
  },
  {
    "path": "lib/models/msg/system.dart",
    "content": "import 'dart:convert';\n\nclass MessageSystemModel {\n  int? id;\n  int? cursor;\n  int? type;\n  String? title;\n  dynamic content;\n  Source? source;\n  String? timeAt;\n  int? cardType;\n  String? cardBrief;\n  String? cardMsgBrief;\n  String? cardCover;\n  String? cardStoryTitle;\n  String? cardLink;\n  String? mc;\n  int? isStation;\n  int? isSend;\n  int? notifyCursor;\n\n  MessageSystemModel({\n    this.id,\n    this.cursor,\n    this.type,\n    this.title,\n    this.content,\n    this.source,\n    this.timeAt,\n    this.cardType,\n    this.cardBrief,\n    this.cardMsgBrief,\n    this.cardCover,\n    this.cardStoryTitle,\n    this.cardLink,\n    this.mc,\n    this.isStation,\n    this.isSend,\n    this.notifyCursor,\n  });\n\n  factory MessageSystemModel.fromJson(Map<String, dynamic> jsons) =>\n      MessageSystemModel(\n        id: jsons[\"id\"],\n        cursor: jsons[\"cursor\"],\n        type: jsons[\"type\"],\n        title: jsons[\"title\"],\n        content: isValidJson(jsons[\"content\"])\n            ? json.decode(jsons[\"content\"])\n            : jsons[\"content\"],\n        source: Source.fromJson(jsons[\"source\"]),\n        timeAt: jsons[\"time_at\"],\n        cardType: jsons[\"card_type\"],\n        cardBrief: jsons[\"card_brief\"],\n        cardMsgBrief: jsons[\"card_msg_brief\"],\n        cardCover: jsons[\"card_cover\"],\n        cardStoryTitle: jsons[\"card_story_title\"],\n        cardLink: jsons[\"card_link\"],\n        mc: jsons[\"mc\"],\n        isStation: jsons[\"is_station\"],\n        isSend: jsons[\"is_send\"],\n        notifyCursor: jsons[\"notify_cursor\"],\n      );\n}\n\nclass Source {\n  String? name;\n  String? logo;\n\n  Source({\n    this.name,\n    this.logo,\n  });\n\n  factory Source.fromJson(Map<String, dynamic> json) => Source(\n        name: json[\"name\"],\n        logo: json[\"logo\"],\n      );\n}\n\nbool isValidJson(String str) {\n  try {\n    json.decode(str);\n  } catch (e) {\n    return false;\n  }\n  return true;\n}\n"
  },
  {
    "path": "lib/models/read/opus.dart",
    "content": "class OpusDataModel {\n  OpusDataModel({\n    this.id,\n    this.detail,\n    this.type,\n    this.theme,\n    this.themeMode,\n  });\n\n  String? id;\n  OpusDetailDataModel? detail;\n  int? type;\n  String? theme;\n  String? themeMode;\n\n  OpusDataModel.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    detail = json['detail'] != null\n        ? OpusDetailDataModel.fromJson(json['detail'])\n        : null;\n    type = json['type'];\n    theme = json['theme'];\n    themeMode = json['themeMode'];\n  }\n}\n\nclass OpusDetailDataModel {\n  OpusDetailDataModel({\n    this.basic,\n    this.idStr,\n    this.modules,\n    this.type,\n  });\n\n  Basic? basic;\n  String? idStr;\n  List<OpusModuleDataModel>? modules;\n  int? type;\n\n  OpusDetailDataModel.fromJson(Map<String, dynamic> json) {\n    basic = json['basic'] != null ? Basic.fromJson(json['basic']) : null;\n    idStr = json['id_str'];\n    if (json['modules'] != null) {\n      modules = <OpusModuleDataModel>[];\n      json['modules'].forEach((v) {\n        modules!.add(OpusModuleDataModel.fromJson(v));\n      });\n    }\n    type = json['type'];\n  }\n}\n\nclass Basic {\n  Basic({\n    this.commentIdStr,\n    this.commentType,\n    this.ridStr,\n    this.title,\n    this.uid,\n  });\n\n  String? commentIdStr;\n  int? commentType;\n  String? ridStr;\n  String? title;\n  int? uid;\n\n  Basic.fromJson(Map<String, dynamic> json) {\n    commentIdStr = json['comment_id_str'];\n    commentType = json['comment_type'];\n    ridStr = json['rid_str'];\n    title = json['title'];\n    uid = json['uid'];\n  }\n}\n\nclass OpusModuleDataModel {\n  OpusModuleDataModel({\n    this.moduleTitle,\n    this.moduleAuthor,\n    this.moduleContent,\n    this.moduleExtend,\n    this.moduleBottom,\n    this.moduleStat,\n  });\n\n  ModuleTop? moduleTop;\n  ModuleTitle? moduleTitle;\n  ModuleAuthor? moduleAuthor;\n  ModuleContent? moduleContent;\n  ModuleExtend? moduleExtend;\n  ModuleBottom? moduleBottom;\n  ModuleStat? moduleStat;\n\n  OpusModuleDataModel.fromJson(Map<String, dynamic> json) {\n    moduleTop = json['module_top'] != null\n        ? ModuleTop.fromJson(json['module_top'])\n        : null;\n    moduleTitle = json['module_title'] != null\n        ? ModuleTitle.fromJson(json['module_title'])\n        : null;\n    moduleAuthor = json['module_author'] != null\n        ? ModuleAuthor.fromJson(json['module_author'])\n        : null;\n    moduleContent = json['module_content'] != null\n        ? ModuleContent.fromJson(json['module_content'])\n        : null;\n    moduleExtend = json['module_extend'] != null\n        ? ModuleExtend.fromJson(json['module_extend'])\n        : null;\n    moduleBottom = json['module_bottom'] != null\n        ? ModuleBottom.fromJson(json['module_bottom'])\n        : null;\n    moduleStat = json['module_stat'] != null\n        ? ModuleStat.fromJson(json['module_stat'])\n        : null;\n  }\n}\n\nclass ModuleTop {\n  ModuleTop({\n    this.type,\n    this.video,\n  });\n\n  int? type;\n  Map? video;\n\n  ModuleTop.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    video = json['video'];\n  }\n}\n\nclass ModuleTitle {\n  ModuleTitle({\n    this.text,\n  });\n\n  String? text;\n\n  ModuleTitle.fromJson(Map<String, dynamic> json) {\n    text = json['text'];\n  }\n}\n\nclass ModuleAuthor {\n  ModuleAuthor({\n    this.face,\n    this.mid,\n    this.name,\n    this.pubTime,\n  });\n\n  String? face;\n  int? mid;\n  String? name;\n  String? pubTime;\n\n  ModuleAuthor.fromJson(Map<String, dynamic> json) {\n    face = json['face'];\n    mid = json['mid'];\n    name = json['name'];\n    pubTime = json['pub_time'];\n  }\n}\n\nclass ModuleContent {\n  ModuleContent({\n    this.paragraphs,\n    this.moduleType,\n  });\n\n  List<ModuleParagraph>? paragraphs;\n  String? moduleType;\n\n  ModuleContent.fromJson(Map<String, dynamic> json) {\n    if (json['paragraphs'] != null) {\n      paragraphs = <ModuleParagraph>[];\n      json['paragraphs'].forEach((v) {\n        paragraphs!.add(ModuleParagraph.fromJson(v));\n      });\n    }\n    moduleType = json['module_type'];\n  }\n}\n\nclass ModuleParagraph {\n  ModuleParagraph({\n    this.align,\n    this.paraType,\n    this.pic,\n    this.text,\n  });\n\n  // 0 左对齐  1 居中  2 右对齐\n  int? align;\n  int? paraType;\n  Pics? pic;\n  ModuleParagraphText? text;\n  LinkCard? linkCard;\n\n  ModuleParagraph.fromJson(Map<String, dynamic> json) {\n    align = json['align'];\n    paraType = json['para_type'] == null && json['link_card'] != null\n        ? 3\n        : json['para_type'];\n    pic = json['pic'] != null ? Pics.fromJson(json['pic']) : null;\n    text = json['text'] != null\n        ? ModuleParagraphText.fromJson(json['text'])\n        : null;\n    linkCard =\n        json['link_card'] != null ? LinkCard.fromJson(json['link_card']) : null;\n  }\n}\n\nclass Pics {\n  Pics({\n    this.pics,\n    this.style,\n  });\n\n  List<Pic>? pics;\n  int? style;\n\n  Pics.fromJson(Map<String, dynamic> json) {\n    if (json['pics'] != null) {\n      pics = <Pic>[];\n      json['pics'].forEach((v) {\n        pics!.add(Pic.fromJson(v));\n      });\n    }\n    style = json['style'];\n  }\n}\n\nclass Pic {\n  Pic({\n    this.height,\n    this.size,\n    this.url,\n    this.width,\n    this.aspectRatio,\n    this.scale,\n  });\n\n  int? height;\n  double? size;\n  String? url;\n  int? width;\n  double? aspectRatio;\n  double? scale;\n\n  Pic.fromJson(Map<String, dynamic> json) {\n    height = json['height'];\n    size = json['size'];\n    url = json['url'];\n    width = json['width'];\n    aspectRatio = json['width'] / json['height'];\n    scale = customDivision(json['width'], 600);\n  }\n}\n\nclass LinkCard {\n  LinkCard({\n    this.cover,\n    this.descSecond,\n    this.duration,\n    this.jumpUrl,\n    this.title,\n  });\n\n  String? cover;\n  String? descSecond;\n  String? duration;\n  String? jumpUrl;\n  String? title;\n\n  LinkCard.fromJson(Map<String, dynamic> json) {\n    cover = json['card']['cover'];\n    descSecond = json['card']['desc_second'];\n    duration = json['card']['duration'];\n    jumpUrl = json['card']['jump_url'];\n    title = json['card']['title'];\n  }\n}\n\nclass ModuleParagraphText {\n  ModuleParagraphText({\n    this.nodes,\n  });\n\n  List<ModuleParagraphTextNode>? nodes;\n\n  ModuleParagraphText.fromJson(Map<String, dynamic> json) {\n    if (json['nodes'] != null) {\n      nodes = <ModuleParagraphTextNode>[];\n      json['nodes'].forEach((v) {\n        nodes!.add(ModuleParagraphTextNode.fromJson(v));\n      });\n    }\n  }\n}\n\nclass ModuleParagraphTextNode {\n  ModuleParagraphTextNode({\n    this.type,\n    this.nodeType,\n    this.word,\n  });\n\n  String? type;\n  int? nodeType;\n  ModuleParagraphTextNodeWord? word;\n\n  ModuleParagraphTextNode.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    nodeType = json['node_type'];\n    word = json['word'] != null\n        ? ModuleParagraphTextNodeWord.fromJson(json['word'])\n        : null;\n  }\n}\n\nclass ModuleParagraphTextNodeWord {\n  ModuleParagraphTextNodeWord({\n    this.color,\n    this.fontSize,\n    this.style,\n    this.words,\n  });\n\n  String? color;\n  int? fontSize;\n  ModuleParagraphTextNodeWordStyle? style;\n  String? words;\n\n  ModuleParagraphTextNodeWord.fromJson(Map<String, dynamic> json) {\n    color = json['color'];\n    fontSize = json['font_size'];\n    style = json['style'] != null\n        ? ModuleParagraphTextNodeWordStyle.fromJson(json['style'])\n        : null;\n    words = json['words'];\n  }\n}\n\nclass ModuleParagraphTextNodeWordStyle {\n  ModuleParagraphTextNodeWordStyle({\n    this.bold,\n  });\n\n  bool? bold;\n\n  ModuleParagraphTextNodeWordStyle.fromJson(Map<String, dynamic> json) {\n    bold = json['bold'];\n  }\n}\n\nclass ModuleExtend {\n  ModuleExtend({\n    this.items,\n  });\n\n  List<ModuleExtendItem>? items;\n\n  ModuleExtend.fromJson(Map<String, dynamic> json) {\n    if (json['items'] != null) {\n      items = <ModuleExtendItem>[];\n      json['items'].forEach((v) {\n        items!.add(ModuleExtendItem.fromJson(v));\n      });\n    }\n  }\n}\n\nclass ModuleExtendItem {\n  ModuleExtendItem({\n    this.bizId,\n    this.bizType,\n    this.icon,\n    this.jumpUrl,\n    this.text,\n  });\n\n  dynamic bizId;\n  int? bizType;\n  dynamic icon;\n  String? jumpUrl;\n  String? text;\n\n  ModuleExtendItem.fromJson(Map<String, dynamic> json) {\n    bizId = json['biz_id'];\n    bizType = json['biz_type'];\n    icon = json['icon'];\n    jumpUrl = json['jump_url'];\n    text = json['text'];\n  }\n}\n\nclass ModuleBottom {\n  ModuleBottom({\n    this.shareInfo,\n  });\n\n  ShareInfo? shareInfo;\n\n  ModuleBottom.fromJson(Map<String, dynamic> json) {\n    shareInfo = json['share_info'] != null\n        ? ShareInfo.fromJson(json['share_info'])\n        : null;\n  }\n}\n\nclass ShareInfo {\n  ShareInfo({\n    this.pic,\n    this.summary,\n    this.title,\n  });\n\n  String? pic;\n  String? summary;\n  String? title;\n\n  ShareInfo.fromJson(Map<String, dynamic> json) {\n    pic = json['pic'];\n    summary = json['summary'];\n    title = json['title'];\n  }\n}\n\nclass ModuleStat {\n  ModuleStat({\n    this.coin,\n    this.comment,\n    this.favorite,\n    this.forward,\n    this.like,\n  });\n\n  StatItem? coin;\n  StatItem? comment;\n  StatItem? favorite;\n  StatItem? forward;\n  StatItem? like;\n\n  ModuleStat.fromJson(Map<String, dynamic> json) {\n    coin = json['coin'] != null ? StatItem.fromJson(json['coin']) : null;\n    comment =\n        json['comment'] != null ? StatItem.fromJson(json['comment']) : null;\n    favorite =\n        json['favorite'] != null ? StatItem.fromJson(json['favorite']) : null;\n    forward =\n        json['forward'] != null ? StatItem.fromJson(json['forward']) : null;\n    like = json['like'] != null ? StatItem.fromJson(json['like']) : null;\n  }\n}\n\nclass StatItem {\n  StatItem({\n    this.count,\n    this.forbidden,\n    this.status,\n  });\n\n  int? count;\n  bool? forbidden;\n  bool? status;\n\n  StatItem.fromJson(Map<String, dynamic> json) {\n    count = json['count'];\n    forbidden = json['forbidden'];\n    status = json['status'];\n  }\n}\n\ndouble customDivision(int a, int b) {\n  double result = a / b;\n  if (result < 1) {\n    return result;\n  } else {\n    return 1.0;\n  }\n}\n"
  },
  {
    "path": "lib/models/read/read.dart",
    "content": "import 'package:pilipala/models/member/info.dart';\n\nimport 'opus.dart';\n\nclass ReadDataModel {\n  ReadDataModel({\n    this.cvid,\n    this.readInfo,\n    this.readViewInfo,\n    this.upInfo,\n    this.catalogList,\n    this.recommendInfoList,\n    this.hiddenInteraction,\n    this.isModern,\n  });\n\n  int? cvid;\n  ReadInfo? readInfo;\n  Map? readViewInfo;\n  Map? upInfo;\n  List<dynamic>? catalogList;\n  List<dynamic>? recommendInfoList;\n  bool? hiddenInteraction;\n  bool? isModern;\n\n  ReadDataModel.fromJson(Map<String, dynamic> json) {\n    cvid = json['cvid'];\n    readInfo =\n        json['readInfo'] != null ? ReadInfo.fromJson(json['readInfo']) : null;\n    readViewInfo = json['readViewInfo'];\n    upInfo = json['upInfo'];\n    if (json['catalogList'] != null) {\n      catalogList = <dynamic>[];\n      json['catalogList'].forEach((v) {\n        catalogList!.add(v);\n      });\n    }\n    if (json['recommendInfoList'] != null) {\n      recommendInfoList = <dynamic>[];\n      json['recommendInfoList'].forEach((v) {\n        recommendInfoList!.add(v);\n      });\n    }\n    hiddenInteraction = json['hiddenInteraction'];\n    isModern = json['isModern'];\n  }\n}\n\nclass ReadInfo {\n  ReadInfo({\n    this.id,\n    this.category,\n    this.title,\n    this.summary,\n    this.bannerUrl,\n    this.author,\n    this.publishTime,\n    this.ctime,\n    this.mtime,\n    this.stats,\n    this.attributes,\n    this.words,\n    this.originImageUrls,\n    this.content,\n    this.opus,\n    this.dynIdStr,\n    this.totalArtNum,\n  });\n\n  int? id;\n  Map? category;\n  String? title;\n  String? summary;\n  String? bannerUrl;\n  Author? author;\n  int? publishTime;\n  int? ctime;\n  int? mtime;\n  Map? stats;\n  int? attributes;\n  int? words;\n  List<String>? originImageUrls;\n  String? content;\n  Opus? opus;\n  String? dynIdStr;\n  int? totalArtNum;\n\n  ReadInfo.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    category = json['category'];\n    title = json['title'];\n    summary = json['summary'];\n    bannerUrl = json['banner_url'];\n    author = Author.fromJson(json['author']);\n    publishTime = json['publish_time'];\n    ctime = json['ctime'];\n    mtime = json['mtime'];\n    stats = json['stats'];\n    attributes = json['attributes'];\n    words = json['words'];\n    if (json['origin_image_urls'] != null) {\n      originImageUrls = <String>[];\n      json['origin_image_urls'].forEach((v) {\n        originImageUrls!.add(v);\n      });\n    }\n    content = json['content'];\n    opus = json['opus'] != null ? Opus.fromJson(json['opus']) : null;\n    dynIdStr = json['dyn_id_str'];\n    totalArtNum = json['total_art_num'];\n  }\n}\n\nclass Author {\n  Author({\n    this.mid,\n    this.name,\n    this.face,\n    this.vip,\n    this.fans,\n    this.level,\n  });\n\n  int? mid;\n  String? name;\n  String? face;\n  Vip? vip;\n  int? fans;\n  int? level;\n\n  Author.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    name = json['name'];\n    face = json['face'];\n    vip = json['vip'] != null ? Vip.fromJson(json['vip']) : null;\n    fans = json['fans'];\n    level = json['level'];\n  }\n}\n\nclass Opus {\n  // \"opus_id\": 976625853207150600,\n  // \"opus_source\": 2,\n  // \"title\": \"真的很想骂人 但又没什么好骂的\",\n  // \"content\": {\n  //     \"paragraphs\": [{\n  //         \"para_type\": 1,\n  //         \"text\": {\n  //             \"nodes\": [{\n  //                 \"node_type\": 1,\n  //                 \"word\": {\n  //                     \"words\": \"21年玩到今年4月的号没了  ow1的时候45的号 玩了三年  后面第9赛季一个英杰5的号（虽然是偷的 但我任何违规行为都没有还是给我封了）  最近玩的号叫velleity  只和队友打天梯以及训练赛 又没了 连带着我一个一把没玩过只玩过一场训练赛的小号也没了  实在是无话可说了...\",\n  //                     \"font_size\": 17,\n  //                     \"style\": {},\n  //                     \"font_level\": \"regular\"\n  //                 }\n  //             }]\n  //         }\n  //     }, {\n  //         \"para_type\": 2,\n  //         \"pic\": {\n  //             \"pics\": [{\n  //                 \"url\": \"https:\\u002F\\u002Fi0.hdslb.com\\u002Fbfs\\u002Fnew_dyn\\u002Fba4e57459451fe74dcb70fd20bde9823316082117.jpg\",\n  //                 \"width\": 1600,\n  //                 \"height\": 1000,\n  //                 \"size\": 588.482421875\n  //             }],\n  //             \"style\": 1\n  //         }\n  //     }, {\n  //         \"para_type\": 1,\n  //         \"text\": {\n  //             \"nodes\": [{\n  //                 \"node_type\": 1,\n  //                 \"word\": {\n  //                     \"words\": \"\\n\",\n  //                     \"font_size\": 17,\n  //                     \"style\": {},\n  //                     \"font_level\": \"regular\"\n  //                 }\n  //             }]\n  //         }\n  //     }, {\n  //         \"para_type\": 2,\n  //         \"pic\": {\n  //             \"pics\": [{\n  //                 \"url\": \"https:\\u002F\\u002Fi0.hdslb.com\\u002Fbfs\\u002Fnew_dyn\\u002F0945be6b621091ddb8189482a87a36fb316082117.jpg\",\n  //                 \"width\": 1600,\n  //                 \"height\": 1002,\n  //                 \"size\": 665.7861328125\n  //             }],\n  //             \"style\": 1\n  //         }\n  //     }, {\n  //         \"para_type\": 1,\n  //         \"text\": {\n  //             \"nodes\": [{\n  //                 \"node_type\": 1,\n  //                 \"word\": {\n  //                     \"words\": \"\\n\",\n  //                     \"font_size\": 17,\n  //                     \"style\": {},\n  //                     \"font_level\": \"regular\"\n  //                 }\n  //             }]\n  //         }\n  //     }, {\n  //         \"para_type\": 2,\n  //         \"pic\": {\n  //             \"pics\": [{\n  //                 \"url\": \"https:\\u002F\\u002Fi0.hdslb.com\\u002Fbfs\\u002Fnew_dyn\\u002Ffa60649f8786578a764a1e68a2c5d23f316082117.jpg\",\n  //                 \"width\": 1600,\n  //                 \"height\": 999,\n  //                 \"size\": 332.970703125\n  //             }],\n  //             \"style\": 1\n  //         }\n  //     }, {\n  //         \"para_type\": 1,\n  //         \"text\": {\n  //             \"nodes\": [{\n  //                 \"node_type\": 1,\n  //                 \"word\": {\n  //                     \"words\": \"\\n\",\n  //                     \"font_size\": 17,\n  //                     \"style\": {},\n  //                     \"font_level\": \"regular\"\n  //                 }\n  //             }]\n  //         }\n  //     }]\n  // },\n  // \"pub_info\": {\n  //     \"uid\": 316082117,\n  //     \"pub_time\": 1726226826\n  // },\n  // \"article\": {\n  //     \"category_id\": 15,\n  //     \"cover\": [{\n  //         \"url\": \"https:\\u002F\\u002Fi0.hdslb.com\\u002Fbfs\\u002Fnew_dyn\\u002Fbanner\\u002Feb10074186a62f98c18e1b5b9deb38be316082117.png\",\n  //         \"width\": 1071,\n  //         \"height\": 315,\n  //         \"size\": 225.625\n  //     }]\n  // },\n  // \"version\": {\n  //     \"cvid\": 38660379,\n  //     \"version_id\": 101683514411343360\n  // }\n  Opus({\n    this.opusId,\n    this.opusSource,\n    this.title,\n    this.content,\n  });\n\n  int? opusId;\n  int? opusSource;\n  String? title;\n  Content? content;\n\n  Opus.fromJson(Map<String, dynamic> json) {\n    opusId = json['opus_id'];\n    opusSource = json['opus_source'];\n    title = json['title'];\n    content =\n        json['content'] != null ? Content.fromJson(json['content']) : null;\n  }\n}\n\nclass Content {\n  Content({\n    this.paragraphs,\n  });\n\n  List<ModuleParagraph>? paragraphs;\n\n  Content.fromJson(Map<String, dynamic> json) {\n    if (json['paragraphs'] != null) {\n      paragraphs = <ModuleParagraph>[];\n      json['paragraphs'].forEach((v) {\n        paragraphs!.add(ModuleParagraph.fromJson(v));\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "lib/models/search/all.dart",
    "content": "class SearchAllModel {\n  SearchAllModel({this.topTList});\n\n  Map? topTList;\n\n  SearchAllModel.fromJson(Map<String, dynamic> json) {\n    topTList = json['top_tlist'];\n  }\n}\n"
  },
  {
    "path": "lib/models/search/hot.dart",
    "content": "class HotSearchModel {\n  HotSearchModel({\n    this.list,\n  });\n\n  List<HotSearchItem>? list;\n\n  HotSearchModel.fromJson(Map<String, dynamic> json) {\n    list = json['list']\n        .map<HotSearchItem>((e) => HotSearchItem.fromJson(e))\n        .toList();\n  }\n}\n\nclass HotSearchItem {\n  HotSearchItem({\n    this.keyword,\n    this.showName,\n    this.wordType,\n    this.icon,\n  });\n\n  String? keyword;\n  String? showName;\n  // 4/5热 11话题 8普通 7直播\n  int? wordType;\n  String? icon;\n\n  HotSearchItem.fromJson(Map<String, dynamic> json) {\n    keyword = json['keyword'];\n    showName = json['show_name'];\n    wordType = json['word_type'];\n    icon = json['icon'];\n  }\n}\n"
  },
  {
    "path": "lib/models/search/result.dart",
    "content": "import 'package:pilipala/utils/em.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass SearchVideoModel {\n  SearchVideoModel({this.list});\n  List<SearchVideoItemModel>? list;\n  SearchVideoModel.fromJson(Map<String, dynamic> json) {\n    list = json['result'] == null\n        ? []\n        : json['result']\n            .where((e) => e['available'] == true)\n            .map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))\n            .toList();\n  }\n}\n\nclass SearchVideoItemModel {\n  SearchVideoItemModel({\n    this.type,\n    this.id,\n    this.cid,\n    // this.author,\n    this.mid,\n    // this.typeid,\n    // this.typename,\n    this.arcurl,\n    this.aid,\n    this.bvid,\n    this.title,\n    this.titleList,\n    this.description,\n    this.pic,\n    // this.play,\n    this.videoReview,\n    // this.favorites,\n    this.tag,\n    // this.review,\n    this.pubdate,\n    this.senddate,\n    this.duration,\n    // this.viewType,\n    // this.like,\n    // this.upic,\n    // this.danmaku,\n    this.owner,\n    this.stat,\n    this.rcmdReason,\n  });\n\n  String? type;\n  int? id;\n  int? cid;\n  // String? author;\n  int? mid;\n  // String? typeid;\n  // String? typename;\n  String? arcurl;\n  int? aid;\n  String? bvid;\n  String? title;\n  List? titleList;\n  String? description;\n  String? pic;\n  // String? play;\n  int? videoReview;\n  // String? favorites;\n  String? tag;\n  // String? review;\n  int? pubdate;\n  int? senddate;\n  int? duration;\n  // String? duration;\n  // String? viewType;\n  // String? like;\n  // String? upic;\n  // String? danmaku;\n  Owner? owner;\n  Stat? stat;\n  String? rcmdReason;\n\n  SearchVideoItemModel.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    id = json['id'];\n    arcurl = json['arcurl'];\n    aid = json['aid'];\n    bvid = json['bvid'];\n    mid = json['mid'];\n    title = json['title'].replaceAll(RegExp(r'<.*?>'), '');\n    // title = Em.regTitle(json['title']);\n    titleList = Em.regTitle(json['title']);\n    description = json['description'];\n    pic = json['pic'] != null && json['pic'].startsWith('//')\n        ? 'https:${json['pic']}'\n        : json['pic'] ?? '';\n    videoReview = json['video_review'];\n    pubdate = json['pubdate'];\n    senddate = json['senddate'];\n    duration = Utils.duration(json['duration']);\n    owner = Owner.fromJson(json);\n    stat = Stat.fromJson(json);\n  }\n}\n\nclass Stat {\n  Stat({\n    this.view,\n    this.danmaku,\n    this.favorite,\n    this.reply,\n    this.like,\n  });\n\n  // 播放量\n  int? view;\n  // 弹幕数\n  int? danmaku;\n  // 收藏数\n  int? favorite;\n  // 评论数\n  int? reply;\n  // 喜欢\n  int? like;\n\n  Stat.fromJson(Map<String, dynamic> json) {\n    view = json['play'];\n    danmaku = json['danmaku'];\n    favorite = json['favorite'];\n    reply = json['review'];\n    like = json['like'];\n  }\n}\n\nclass Owner {\n  Owner({\n    this.mid,\n    this.name,\n    this.face,\n  });\n  int? mid;\n  String? name;\n  String? face;\n\n  Owner.fromJson(Map<String, dynamic> json) {\n    mid = json[\"mid\"];\n    name = json[\"author\"];\n    face = json['upic'];\n  }\n}\n\nclass SearchUserModel {\n  SearchUserModel({this.list});\n  List<SearchUserItemModel>? list;\n  SearchUserModel.fromJson(Map<String, dynamic> json) {\n    list = json['result']\n        .map<SearchUserItemModel>((e) => SearchUserItemModel.fromJson(e))\n        .toList();\n  }\n}\n\nclass SearchUserItemModel {\n  SearchUserItemModel({\n    this.type,\n    this.mid,\n    this.uname,\n    this.usign,\n    this.fans,\n    this.videos,\n    this.upic,\n    this.faceNft,\n    this.faceNftType,\n    this.verifyInfo,\n    this.level,\n    this.gender,\n    this.isUpUser,\n    this.isLive,\n    this.roomId,\n    this.officialVerify,\n  });\n\n  String? type;\n  int? mid;\n  String? uname;\n  String? usign;\n  int? fans;\n  int? videos;\n  String? upic;\n  int? faceNft;\n  int? faceNftType;\n  String? verifyInfo;\n  int? level;\n  int? gender;\n  int? isUpUser;\n  int? isLive;\n  int? roomId;\n  Map? officialVerify;\n\n  SearchUserItemModel.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    mid = json['mid'];\n    uname = json['uname'];\n    usign = json['usign'];\n    fans = json['fans'];\n    videos = json['videos'];\n    upic = 'https:${json['upic']}';\n    faceNft = json['face_nft'];\n    faceNftType = json['face_nft_type'];\n    verifyInfo = json['verify_info'];\n    level = json['level'];\n    gender = json['gender'];\n    isUpUser = json['is_upuser'];\n    isLive = json['is_live'];\n    roomId = json['room_id'];\n    officialVerify = json['official_verify'];\n  }\n}\n\nclass SearchLiveModel {\n  SearchLiveModel({this.list});\n  List<SearchLiveItemModel>? list;\n  SearchLiveModel.fromJson(Map<String, dynamic> json) {\n    list = json['result']\n        .map<SearchLiveItemModel>((e) => SearchLiveItemModel.fromJson(e))\n        .toList();\n  }\n}\n\nclass SearchLiveItemModel {\n  SearchLiveItemModel({\n    this.rankOffset,\n    this.uid,\n    this.tags,\n    this.liveTime,\n    this.uname,\n    this.uface,\n    this.face,\n    this.userCover,\n    this.type,\n    this.title,\n    this.titleList,\n    this.cover,\n    this.pic,\n    this.online,\n    this.rankIndex,\n    this.rankScore,\n    this.roomid,\n    this.attentions,\n    this.cateName,\n  });\n\n  int? rankOffset;\n  int? uid;\n  String? tags;\n  String? liveTime;\n  String? uname;\n  String? uface;\n  String? face;\n  String? userCover;\n  String? type;\n  String? title;\n  List? titleList;\n  String? cover;\n  String? pic;\n  int? online;\n  int? rankIndex;\n  int? rankScore;\n  int? roomid;\n  int? attentions;\n  String? cateName;\n  Map? watchedShow;\n\n  SearchLiveItemModel.fromJson(Map<String, dynamic> json) {\n    rankOffset = json['rank_offset'];\n    uid = json['uid'];\n    tags = json['tags'];\n    liveTime = json['live_time'];\n    uname = json['uname'];\n    uface = json['uface'];\n    face = json['uface'];\n    userCover = json['user_cover'];\n    type = json['type'];\n    title = json['title'].replaceAll(RegExp(r'<.*?>'), '');\n    titleList = Em.regTitle(json['title']);\n    cover = json['cover'];\n    pic = json['cover'];\n    online = json['online'];\n    rankIndex = json['rank_index'];\n    rankScore = json['rank_score'];\n    roomid = json['roomid'];\n    attentions = json['attentions'];\n    cateName = Em.regCate(json['cate_name']) ?? '';\n  }\n}\n\nclass SearchMBangumiModel {\n  SearchMBangumiModel({this.list});\n  List<SearchMBangumiItemModel>? list;\n  SearchMBangumiModel.fromJson(Map<String, dynamic> json) {\n    list = json['result'] != null\n        ? json['result']\n            .map<SearchMBangumiItemModel>(\n                (e) => SearchMBangumiItemModel.fromJson(e))\n            .toList()\n        : [];\n  }\n}\n\nclass SearchMBangumiItemModel {\n  SearchMBangumiItemModel({\n    this.type,\n    this.mediaId,\n    this.title,\n    this.titleList,\n    this.orgTitle,\n    this.mediaType,\n    this.cv,\n    this.staff,\n    this.seasonId,\n    this.isAvid,\n    this.hitEpids,\n    this.seasonType,\n    this.seasonTypeName,\n    this.url,\n    this.buttonText,\n    this.isFollow,\n    this.isSelection,\n    this.cover,\n    this.areas,\n    this.styles,\n    this.gotoUrl,\n    this.desc,\n    this.pubtime,\n    this.mediaMode,\n    this.mediaScore,\n    this.indexShow,\n  });\n\n  String? type;\n  int? mediaId;\n  String? title;\n  List? titleList;\n  String? orgTitle;\n  int? mediaType;\n  String? cv;\n  String? staff;\n  int? seasonId;\n  bool? isAvid;\n  String? hitEpids;\n  int? seasonType;\n  String? seasonTypeName;\n  String? url;\n  String? buttonText;\n  int? isFollow;\n  int? isSelection;\n  String? cover;\n  String? areas;\n  String? styles;\n  String? gotoUrl;\n  String? desc;\n  int? pubtime;\n  int? mediaMode;\n  Map? mediaScore;\n  String? indexShow;\n\n  SearchMBangumiItemModel.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    mediaId = json['media_id'];\n    title = json['title'].replaceAll(RegExp(r'<.*?>'), '');\n    titleList = Em.regTitle(json['title']);\n    orgTitle = json['org_title'];\n    mediaType = json['media_type'];\n    cv = json['cv'];\n    staff = json['staff'];\n    seasonId = json['season_id'];\n    isAvid = json['is_avid'];\n    hitEpids = json['hit_epids'];\n    seasonType = json['season_type'];\n    seasonTypeName = json['season_type_name'];\n    url = json['url'];\n    buttonText = json['button_text'];\n    isFollow = json['is_follow'];\n    isSelection = json['is_selection'];\n    cover = json['cover'];\n    areas = json['areas'];\n    styles = json['styles'];\n    gotoUrl = json['goto_url'];\n    desc = json['desc'];\n    pubtime = json['pubtime'];\n    mediaMode = json['media_mode'];\n    mediaScore = json['media_score'];\n    indexShow = json['index_show'];\n  }\n}\n\nclass SearchArticleModel {\n  SearchArticleModel({this.list});\n\n  List<SearchArticleItemModel>? list;\n\n  SearchArticleModel.fromJson(Map<String, dynamic> json) {\n    list = json['result'] != null\n        ? json['result']\n            .map<SearchArticleItemModel>(\n                (e) => SearchArticleItemModel.fromJson(e))\n            .toList()\n        : [];\n  }\n}\n\nclass SearchArticleItemModel {\n  SearchArticleItemModel({\n    this.pubTime,\n    this.like,\n    this.title,\n    this.subTitle,\n    this.rankOffset,\n    this.mid,\n    this.imageUrls,\n    this.id,\n    this.categoryId,\n    this.view,\n    this.reply,\n    this.desc,\n    this.rankScore,\n    this.type,\n    this.templateId,\n    this.categoryName,\n  });\n\n  int? pubTime;\n  int? like;\n  List? title;\n  String? subTitle;\n  int? rankOffset;\n  int? mid;\n  List? imageUrls;\n  int? id;\n  int? categoryId;\n  int? view;\n  int? reply;\n  String? desc;\n  int? rankScore;\n  String? type;\n  int? templateId;\n  String? categoryName;\n\n  SearchArticleItemModel.fromJson(Map<String, dynamic> json) {\n    pubTime = json['pub_time'];\n    like = json['like'];\n    title = Em.regTitle(json['title']);\n    subTitle =\n        Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), ''));\n    rankOffset = json['rank_offset'];\n    mid = json['mid'];\n    imageUrls = json['image_urls'];\n    id = json['id'];\n    categoryId = json['category_id'];\n    view = json['view'];\n    reply = json['reply'];\n    desc = json['desc'];\n    rankScore = json['rank_score'];\n    type = json['type'];\n    templateId = json['templateId'];\n    categoryName = json['category_name'];\n  }\n}\n"
  },
  {
    "path": "lib/models/search/suggest.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\n\nclass SearchSuggestModel {\n  SearchSuggestModel({\n    this.tag,\n    this.term,\n  });\n\n  List<SearchSuggestItem>? tag;\n  String? term;\n\n  SearchSuggestModel.fromJson(Map<String, dynamic> json) {\n    tag = json['tag']\n        .map<SearchSuggestItem>(\n            (e) => SearchSuggestItem.fromJson(e, json['term']))\n        .toList();\n  }\n}\n\nclass SearchSuggestItem {\n  SearchSuggestItem({\n    this.value,\n    this.term,\n    this.spid,\n    this.textRich,\n  });\n\n  String? value;\n  String? term;\n  int? spid;\n  Widget? textRich;\n\n  SearchSuggestItem.fromJson(Map<String, dynamic> json, String inputTerm) {\n    value = json['value'];\n    term = json['term'];\n    textRich = highlightText(json['name']);\n  }\n}\n\nWidget highlightText(String str) {\n  // 创建正则表达式，匹配 <em class=\"suggest_high_light\">...</em> 格式的文本\n  RegExp regex = RegExp(r'<em class=\"suggest_high_light\">(.*?)<\\/em>');\n\n  // 用于存储每个匹配项的列表\n  List<InlineSpan> children = [];\n\n  // 获取所有匹配项\n  Iterable<Match> matches = regex.allMatches(str);\n\n  // 当前索引位置\n  int currentIndex = 0;\n\n  // 遍历每个匹配项\n  for (var match in matches) {\n    // 获取当前匹配项之前的普通文本部分\n    String normalText = str.substring(currentIndex, match.start);\n\n    // 获取需要高亮显示的文本部分\n    String highlightedText = match.group(1)!;\n\n    // 如果普通文本部分不为空，则将其添加到 children 列表中\n    if (normalText.isNotEmpty) {\n      children.add(TextSpan(\n        text: normalText,\n        style: DefaultTextStyle.of(Get.context!).style,\n      ));\n    }\n\n    // 将需要高亮显示的文本部分添加到 children 列表中，并设置相应样式\n    children.add(TextSpan(\n      text: highlightedText,\n      style: TextStyle(\n          fontWeight: FontWeight.bold,\n          color: Theme.of(Get.context!).colorScheme.primary),\n    ));\n\n    // 更新当前索引位置\n    currentIndex = match.end;\n  }\n\n  // 如果当前索引位置小于文本长度，表示还有剩余的普通文本部分\n  if (currentIndex < str.length) {\n    String remainingText = str.substring(currentIndex);\n\n    // 将剩余的普通文本部分添加到 children 列表中\n    children.add(TextSpan(\n      text: remainingText,\n      style: DefaultTextStyle.of(Get.context!).style,\n    ));\n  }\n\n  // 使用 Text.rich 创建包含高亮显示的富文本小部件，并返回\n  return Text.rich(TextSpan(children: children));\n}\n"
  },
  {
    "path": "lib/models/user/black.dart",
    "content": "class BlackListDataModel {\n  BlackListDataModel({\n    this.list,\n    this.total,\n  });\n\n  List<BlackListItem>? list;\n  int? total;\n\n  BlackListDataModel.fromJson(Map<String, dynamic> json) {\n    list = json['list']\n        .map<BlackListItem>((e) => BlackListItem.fromJson(e))\n        .toList();\n    total = json['total'];\n  }\n}\n\nclass BlackListItem {\n  BlackListItem({\n    this.face,\n    this.mid,\n    this.mtime,\n    this.uname,\n  });\n\n  String? face;\n  int? mid;\n  int? mtime;\n  String? uname;\n\n  BlackListItem.fromJson(Map<String, dynamic> json) {\n    face = json['face'];\n    mid = json['mid'];\n    mtime = json['mtime'];\n    uname = json['uname'];\n  }\n}\n"
  },
  {
    "path": "lib/models/user/fav_detail.dart",
    "content": "import 'package:pilipala/models/model_owner.dart';\n\nclass FavDetailData {\n  FavDetailData({\n    this.info,\n    this.medias,\n    this.hasMore,\n  });\n\n  Map? info;\n  List<FavDetailItemData>? medias;\n  bool? hasMore;\n\n  FavDetailData.fromJson(Map<String, dynamic> json) {\n    info = json['info'];\n    medias = json['medias'] != null\n        ? json['medias']\n            .map<FavDetailItemData>((e) => FavDetailItemData.fromJson(e))\n            .toList()\n        : [];\n    hasMore = json['has_more'];\n  }\n}\n\nclass FavDetailItemData {\n  FavDetailItemData({\n    this.id,\n    this.type,\n    this.title,\n    this.pic,\n    this.intro,\n    this.page,\n    this.duration,\n    this.owner,\n    this.attr,\n    this.cntInfo,\n    this.link,\n    this.ctime,\n    this.pubdate,\n    this.favTime,\n    this.bvId,\n    this.bvid,\n    // this.season,\n    this.ogv,\n    this.stat,\n    this.cid,\n    this.epId,\n  });\n\n  int? id;\n  int? type;\n  String? title;\n  String? pic;\n  String? intro;\n  int? page;\n  int? duration;\n  Owner? owner;\n  int? attr;\n  Map? cntInfo;\n  String? link;\n  int? ctime;\n  int? pubdate;\n  int? favTime;\n  String? bvId;\n  String? bvid;\n  Map? ogv;\n  Stat? stat;\n  int? cid;\n  String? epId;\n\n  FavDetailItemData.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    type = json['type'];\n    title = json['title'];\n    pic = json['cover'];\n    intro = json['intro'];\n    page = json['page'];\n    duration = json['duration'];\n    owner = Owner.fromJson(json['upper']);\n    attr = json['attr'];\n    cntInfo = json['cnt_info'];\n    link = json['link'];\n    ctime = json['ctime'];\n    pubdate = json['pubtime'];\n    favTime = json['fav_time'];\n    bvId = json['bv_id'];\n    bvid = json['bvid'];\n    ogv = json['ogv'];\n    stat = Stat.fromJson(json['cnt_info']);\n    cid = json['ugc'] != null ? json['ugc']['first_cid'] : null;\n    if (json['link'] != null && json['link'].contains('/bangumi')) {\n      epId = resolveEpId(json['link']);\n    }\n  }\n\n  String resolveEpId(url) {\n    RegExp regex = RegExp(r'\\d+');\n    Iterable<Match> matches = regex.allMatches(url);\n    List<String> numbers = [];\n    for (Match match in matches) {\n      numbers.add(match.group(0)!);\n    }\n    return numbers[0];\n  }\n}\n\nclass Stat {\n  Stat({\n    this.view,\n    this.danmaku,\n  });\n\n  int? view;\n  int? danmaku;\n\n  Stat.fromJson(Map<String, dynamic> json) {\n    view = json['play'];\n    danmaku = json['danmaku'];\n  }\n}\n"
  },
  {
    "path": "lib/models/user/fav_folder.dart",
    "content": "class FavFolderData {\n  FavFolderData({\n    this.count,\n    this.list,\n    this.hasMore,\n  });\n\n  int? count;\n  List<FavFolderItemData>? list;\n  bool? hasMore;\n\n  FavFolderData.fromJson(Map<String, dynamic> json) {\n    count = json['count'];\n    list = json['list'] != null\n        ? json['list']\n            .map<FavFolderItemData>((e) => FavFolderItemData.fromJson(e))\n            .toList()\n        : <FavFolderItemData>[];\n    hasMore = json['has_more'];\n  }\n}\n\nclass FavFolderItemData {\n  FavFolderItemData({\n    this.id,\n    this.fid,\n    this.mid,\n    this.attr,\n    this.title,\n    this.cover,\n    this.upper,\n    this.coverType,\n    this.intro,\n    this.ctime,\n    this.mtime,\n    this.state,\n    this.favState,\n    this.mediaCount,\n    this.viewCount,\n    this.vt,\n    this.playSwitch,\n    this.type,\n    this.link,\n    this.bvid,\n  });\n\n  int? id;\n  int? fid;\n  int? mid;\n  int? attr;\n  String? title;\n  String? cover;\n  Upper? upper;\n  int? coverType;\n  String? intro;\n  int? ctime;\n  int? mtime;\n  int? state;\n  int? favState;\n  int? mediaCount;\n  int? viewCount;\n  int? vt;\n  int? playSwitch;\n  int? type;\n  String? link;\n  String? bvid;\n\n  FavFolderItemData.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    fid = json['fid'];\n    mid = json['mid'];\n    attr = json['attr'];\n    title = json['title'];\n    cover = json['cover'];\n    upper = json['upper'] != null ? Upper.fromJson(json['upper']) : Upper();\n    coverType = json['cover_type'];\n    intro = json['intro'];\n    ctime = json['ctime'];\n    mtime = json['mtime'];\n    state = json['state'];\n    favState = json['fav_state'];\n    mediaCount = json['media_count'];\n    viewCount = json['view_count'];\n    vt = json['vt'];\n    playSwitch = json['play_switch'];\n    type = json['type'];\n    link = json['link'];\n    bvid = json['bvid'];\n  }\n}\n\nclass Upper {\n  Upper({\n    this.mid,\n    this.name,\n    this.face,\n  });\n\n  int? mid;\n  String? name;\n  String? face;\n\n  Upper.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    name = json['name'];\n    face = json['face'];\n  }\n}\n"
  },
  {
    "path": "lib/models/user/history.dart",
    "content": "class HistoryData {\n  HistoryData({\n    this.cursor,\n    this.tab,\n    this.list,\n    this.page,\n  });\n\n  Cursor? cursor;\n  List<HisTabItem>? tab;\n  List<HisListItem>? list;\n  Map? page;\n\n  HistoryData.fromJson(Map<String, dynamic> json) {\n    cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;\n    tab = json['tab'] != null\n        ? json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList()\n        : [];\n    list = json['list'] != null\n        ? json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList()\n        : [];\n    page = json['page'];\n  }\n}\n\nclass Cursor {\n  Cursor({\n    this.max,\n    this.viewAt,\n    this.business,\n    this.ps,\n  });\n\n  int? max;\n  int? viewAt;\n  String? business;\n  int? ps;\n\n  Cursor.fromJson(Map<String, dynamic> json) {\n    max = json['max'];\n    viewAt = json['view_at'];\n    business = json['business'];\n    ps = json['ps'];\n  }\n}\n\nclass HisTabItem {\n  HisTabItem({\n    this.type,\n    this.name,\n  });\n\n  String? type;\n  String? name;\n\n  HisTabItem.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    name = json['name'];\n  }\n}\n\nclass HisListItem {\n  HisListItem({\n    this.title,\n    this.longTitle,\n    this.cover,\n    this.pic,\n    this.covers,\n    this.uri,\n    this.history,\n    this.videos,\n    this.authorName,\n    this.authorFace,\n    this.authorMid,\n    this.viewAt,\n    this.progress,\n    this.badge,\n    this.showTitle,\n    this.duration,\n    this.current,\n    this.total,\n    this.newDesc,\n    this.isFinish,\n    this.isFav,\n    this.kid,\n    this.tagName,\n    this.liveStatus,\n    this.checked,\n  });\n\n  String? title;\n  String? longTitle;\n  String? cover;\n  String? pic;\n  List? covers;\n  String? uri;\n  History? history;\n  int? videos;\n  String? authorName;\n  String? authorFace;\n  int? authorMid;\n  int? viewAt;\n  int? progress;\n  String? badge;\n  String? showTitle;\n  int? duration;\n  String? current;\n  int? total;\n  String? newDesc;\n  int? isFinish;\n  int? isFav;\n  int? kid;\n  String? tagName;\n  int? liveStatus;\n  bool? checked;\n\n  HisListItem.fromJson(Map<String, dynamic> json) {\n    title = json['title'];\n    longTitle = json['long_title'];\n    cover = json['cover'];\n    pic = json['cover'] ?? '';\n    covers = json['covers'] ?? [];\n    uri = json['uri'];\n    history = History.fromJson(json['history']);\n    videos = json['videos'];\n    authorName = json['author_name'];\n    authorFace = json['author_face'];\n    authorMid = json['author_mid'];\n    viewAt = json['view_at'];\n    progress = json['progress'];\n    badge = json['badge'];\n    showTitle = json['show_title'] == '' ? null : json['show_title'];\n    duration = json['duration'];\n    current = json['current'];\n    total = json['total'];\n    newDesc = json['new_desc'];\n    isFinish = json['is_finish'];\n    isFav = json['is_fav'];\n    kid = json['kid'];\n    tagName = json['tag_name'];\n    liveStatus = json['live_status'];\n    checked = false;\n  }\n}\n\nclass History {\n  History({\n    this.oid,\n    this.epid,\n    this.bvid,\n    this.page,\n    this.cid,\n    this.part,\n    this.business,\n    this.dt,\n  });\n\n  int? oid;\n  int? epid;\n  String? bvid;\n  int? page;\n  int? cid;\n  String? part;\n  String? business;\n  int? dt;\n\n  History.fromJson(Map<String, dynamic> json) {\n    oid = json['oid'];\n    epid = json['epid'];\n    bvid = json['bvid'] == '' ? null : json['bvid'];\n    page = json['page'];\n    cid = json['cid'] == 0 ? null : json['cid'];\n    part = json['part'];\n    business = json['business'];\n    dt = json['dt'];\n  }\n}\n"
  },
  {
    "path": "lib/models/user/info.dart",
    "content": "import 'package:hive/hive.dart';\n\npart 'info.g.dart';\n\n@HiveType(typeId: 4)\nclass UserInfoData {\n  UserInfoData({\n    this.isLogin,\n    this.emailVerified,\n    this.face,\n    this.levelInfo,\n    this.mid,\n    this.mobileVerified,\n    this.money,\n    this.moral,\n    this.official,\n    this.officialVerify,\n    this.pendant,\n    this.scores,\n    this.uname,\n    this.vipDueDate,\n    this.vipStatus,\n    this.vipType,\n    this.vipPayType,\n    this.vipThemeType,\n    this.vipLabel,\n    this.vipAvatarSub,\n    this.vipNicknameColor,\n    this.wallet,\n    this.hasShop,\n    this.shopUrl,\n  });\n  @HiveField(0)\n  bool? isLogin;\n  @HiveField(1)\n  int? emailVerified;\n  @HiveField(2)\n  String? face;\n  @HiveField(3)\n  LevelInfo? levelInfo;\n  @HiveField(4)\n  int? mid;\n  @HiveField(5)\n  int? mobileVerified;\n  @HiveField(6)\n  double? money;\n  @HiveField(7)\n  int? moral;\n  @HiveField(8)\n  Map? official;\n  @HiveField(9)\n  Map? officialVerify;\n  @HiveField(10)\n  Map? pendant;\n  @HiveField(11)\n  int? scores;\n  @HiveField(12)\n  String? uname;\n  @HiveField(13)\n  int? vipDueDate;\n  @HiveField(14)\n  int? vipStatus;\n  @HiveField(15)\n  int? vipType;\n  @HiveField(16)\n  int? vipPayType;\n  @HiveField(17)\n  int? vipThemeType;\n  @HiveField(18)\n  Map? vipLabel;\n  @HiveField(19)\n  int? vipAvatarSub;\n  @HiveField(20)\n  String? vipNicknameColor;\n  @HiveField(21)\n  Map? wallet;\n  @HiveField(22)\n  bool? hasShop;\n  @HiveField(23)\n  String? shopUrl;\n\n  UserInfoData.fromJson(Map<String, dynamic> json) {\n    isLogin = json['isLogin'] ?? false;\n    emailVerified = json['email_verified'];\n    face = json['face'];\n    levelInfo = json['level_info'] != null\n        ? LevelInfo.fromJson(json['level_info'])\n        : LevelInfo();\n    mid = json['mid'];\n    mobileVerified = json['mobile_verified'];\n    money = json['money'] is int ? json['money'].toDouble() : json['money'];\n    moral = json['moral'];\n    official = json['official'];\n    officialVerify = json['officialVerify'];\n    pendant = json['pendant'];\n    scores = json['scores'];\n    uname = json['uname'];\n    vipDueDate = json['vipDueDate'];\n    vipStatus = json['vipStatus'];\n    vipType = json['vipType'];\n    vipPayType = json['vip_pay_type'];\n    vipThemeType = json['vip_theme_type'];\n    vipLabel = json['vip_label'];\n    vipAvatarSub = json['vip_avatar_subscript'];\n    vipNicknameColor = json['vip_nickname_color'];\n    wallet = json['wallet'];\n    hasShop = json['has_shop'];\n    shopUrl = json['shop_url'];\n  }\n}\n\n@HiveType(typeId: 5)\nclass LevelInfo {\n  LevelInfo({\n    this.currentLevel,\n    this.currentMin,\n    this.currentExp,\n    this.nextExp,\n  });\n  @HiveField(0)\n  int? currentLevel;\n  @HiveField(1)\n  int? currentMin;\n  @HiveField(2)\n  int? currentExp;\n  @HiveField(3)\n  int? nextExp;\n\n  LevelInfo.fromJson(Map<String, dynamic> json) {\n    currentLevel = json['current_level'];\n    currentMin = json['current_min'];\n    currentExp = json['current_exp'];\n    nextExp =\n        json['current_level'] == 6 ? json['current_exp'] : json['next_exp'];\n  }\n}\n"
  },
  {
    "path": "lib/models/user/info.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'info.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass UserInfoDataAdapter extends TypeAdapter<UserInfoData> {\n  @override\n  final int typeId = 4;\n\n  @override\n  UserInfoData read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return UserInfoData(\n      isLogin: fields[0] as bool?,\n      emailVerified: fields[1] as int?,\n      face: fields[2] as String?,\n      levelInfo: fields[3] as LevelInfo?,\n      mid: fields[4] as int?,\n      mobileVerified: fields[5] as int?,\n      money: fields[6] as double?,\n      moral: fields[7] as int?,\n      official: (fields[8] as Map?)?.cast<dynamic, dynamic>(),\n      officialVerify: (fields[9] as Map?)?.cast<dynamic, dynamic>(),\n      pendant: (fields[10] as Map?)?.cast<dynamic, dynamic>(),\n      scores: fields[11] as int?,\n      uname: fields[12] as String?,\n      vipDueDate: fields[13] as int?,\n      vipStatus: fields[14] as int?,\n      vipType: fields[15] as int?,\n      vipPayType: fields[16] as int?,\n      vipThemeType: fields[17] as int?,\n      vipLabel: (fields[18] as Map?)?.cast<dynamic, dynamic>(),\n      vipAvatarSub: fields[19] as int?,\n      vipNicknameColor: fields[20] as String?,\n      wallet: (fields[21] as Map?)?.cast<dynamic, dynamic>(),\n      hasShop: fields[22] as bool?,\n      shopUrl: fields[23] as String?,\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, UserInfoData obj) {\n    writer\n      ..writeByte(24)\n      ..writeByte(0)\n      ..write(obj.isLogin)\n      ..writeByte(1)\n      ..write(obj.emailVerified)\n      ..writeByte(2)\n      ..write(obj.face)\n      ..writeByte(3)\n      ..write(obj.levelInfo)\n      ..writeByte(4)\n      ..write(obj.mid)\n      ..writeByte(5)\n      ..write(obj.mobileVerified)\n      ..writeByte(6)\n      ..write(obj.money)\n      ..writeByte(7)\n      ..write(obj.moral)\n      ..writeByte(8)\n      ..write(obj.official)\n      ..writeByte(9)\n      ..write(obj.officialVerify)\n      ..writeByte(10)\n      ..write(obj.pendant)\n      ..writeByte(11)\n      ..write(obj.scores)\n      ..writeByte(12)\n      ..write(obj.uname)\n      ..writeByte(13)\n      ..write(obj.vipDueDate)\n      ..writeByte(14)\n      ..write(obj.vipStatus)\n      ..writeByte(15)\n      ..write(obj.vipType)\n      ..writeByte(16)\n      ..write(obj.vipPayType)\n      ..writeByte(17)\n      ..write(obj.vipThemeType)\n      ..writeByte(18)\n      ..write(obj.vipLabel)\n      ..writeByte(19)\n      ..write(obj.vipAvatarSub)\n      ..writeByte(20)\n      ..write(obj.vipNicknameColor)\n      ..writeByte(21)\n      ..write(obj.wallet)\n      ..writeByte(22)\n      ..write(obj.hasShop)\n      ..writeByte(23)\n      ..write(obj.shopUrl);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is UserInfoDataAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass LevelInfoAdapter extends TypeAdapter<LevelInfo> {\n  @override\n  final int typeId = 5;\n\n  @override\n  LevelInfo read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return LevelInfo(\n      currentLevel: fields[0] as int?,\n      currentMin: fields[1] as int?,\n      currentExp: fields[2] as int?,\n      nextExp: fields[3] as int?,\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, LevelInfo obj) {\n    writer\n      ..writeByte(4)\n      ..writeByte(0)\n      ..write(obj.currentLevel)\n      ..writeByte(1)\n      ..write(obj.currentMin)\n      ..writeByte(2)\n      ..write(obj.currentExp)\n      ..writeByte(3)\n      ..write(obj.nextExp);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is LevelInfoAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/models/user/stat.dart",
    "content": "class UserStat {\n  UserStat({\n    this.following,\n    this.follower,\n    this.dynamicCount,\n  });\n\n  int? following;\n  int? follower;\n  int? dynamicCount;\n\n  UserStat.fromJson(Map<String, dynamic> json) {\n    following = json['following'];\n    follower = json['follower'];\n    dynamicCount = json['dynamic_count'];\n  }\n}\n"
  },
  {
    "path": "lib/models/user/sub_detail.dart",
    "content": "class SubDetailModelData {\n  DetailInfo? info;\n  List<SubDetailMediaItem>? medias;\n\n  SubDetailModelData({this.info, this.medias});\n\n  SubDetailModelData.fromJson(Map<String, dynamic> json) {\n    info = DetailInfo.fromJson(json['info']);\n    if (json['medias'] != null) {\n      medias = <SubDetailMediaItem>[];\n      json['medias'].forEach((v) {\n        medias!.add(SubDetailMediaItem.fromJson(v));\n      });\n    }\n  }\n}\n\nclass SubDetailMediaItem {\n  int? id;\n  String? title;\n  String? cover;\n  String? pic;\n  int? duration;\n  int? pubtime;\n  String? bvid;\n  Map? upper;\n  Map? cntInfo;\n  int? enableVt;\n  String? vtDisplay;\n\n  SubDetailMediaItem({\n    this.id,\n    this.title,\n    this.cover,\n    this.pic,\n    this.duration,\n    this.pubtime,\n    this.bvid,\n    this.upper,\n    this.cntInfo,\n    this.enableVt,\n    this.vtDisplay,\n  });\n\n  SubDetailMediaItem.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    title = json['title'];\n    cover = json['cover'];\n    pic = json['cover'];\n    duration = json['duration'];\n    pubtime = json['pubtime'];\n    bvid = json['bvid'];\n    upper = json['upper'];\n    cntInfo = json['cnt_info'];\n    enableVt = json['enable_vt'];\n    vtDisplay = json['vt_display'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final data = <String, dynamic>{};\n    data['id'] = id;\n    data['title'] = title;\n    data['cover'] = cover;\n    data['duration'] = duration;\n    data['pubtime'] = pubtime;\n    data['bvid'] = bvid;\n    data['upper'] = upper;\n    data['cnt_info'] = cntInfo;\n    data['enable_vt'] = enableVt;\n    data['vt_display'] = vtDisplay;\n    return data;\n  }\n}\n\nclass DetailInfo {\n  int? id;\n  int? seasonType;\n  String? title;\n  String? cover;\n  Map? upper;\n  Map? cntInfo;\n  int? mediaCount;\n  String? intro;\n  int? enableVt;\n\n  DetailInfo({\n    this.id,\n    this.seasonType,\n    this.title,\n    this.cover,\n    this.upper,\n    this.cntInfo,\n    this.mediaCount,\n    this.intro,\n    this.enableVt,\n  });\n\n  DetailInfo.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    seasonType = json['season_type'];\n    title = json['title'];\n    cover = json['cover'];\n    upper = json['upper'];\n    cntInfo = json['cnt_info'];\n    mediaCount = json['media_count'];\n    intro = json['intro'];\n    enableVt = json['enable_vt'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final data = <String, dynamic>{};\n    data['id'] = id;\n    data['season_type'] = seasonType;\n    data['title'] = title;\n    data['cover'] = cover;\n    data['upper'] = upper;\n    data['cnt_info'] = cntInfo;\n    data['media_count'] = mediaCount;\n    data['intro'] = intro;\n    data['enable_vt'] = enableVt;\n    return data;\n  }\n}\n"
  },
  {
    "path": "lib/models/user/sub_folder.dart",
    "content": "class SubFolderModelData {\n  final int? count;\n  final List<SubFolderItemData>? list;\n\n  SubFolderModelData({\n    this.count,\n    this.list,\n  });\n\n  factory SubFolderModelData.fromJson(Map<String, dynamic> json) {\n    return SubFolderModelData(\n      count: json['count'],\n      list: json['list'] != null\n          ? (json['list'] as List)\n              .map<SubFolderItemData>((i) => SubFolderItemData.fromJson(i))\n              .toList()\n          : null,\n    );\n  }\n}\n\nclass SubFolderItemData {\n  final int? id;\n  final int? fid;\n  final int? mid;\n  final int? attr;\n  final String? title;\n  final String? cover;\n  final Upper? upper;\n  final int? coverType;\n  final String? intro;\n  final int? ctime;\n  final int? mtime;\n  final int? state;\n  final int? favState;\n  final int? mediaCount;\n  final int? viewCount;\n  final int? vt;\n  final int? playSwitch;\n  final int? type;\n  final String? link;\n  final String? bvid;\n\n  SubFolderItemData({\n    this.id,\n    this.fid,\n    this.mid,\n    this.attr,\n    this.title,\n    this.cover,\n    this.upper,\n    this.coverType,\n    this.intro,\n    this.ctime,\n    this.mtime,\n    this.state,\n    this.favState,\n    this.mediaCount,\n    this.viewCount,\n    this.vt,\n    this.playSwitch,\n    this.type,\n    this.link,\n    this.bvid,\n  });\n\n  factory SubFolderItemData.fromJson(Map<String, dynamic> json) {\n    return SubFolderItemData(\n      id: json['id'],\n      fid: json['fid'],\n      mid: json['mid'],\n      attr: json['attr'],\n      title: json['title'],\n      cover: json['cover'],\n      upper: json['upper'] != null ? Upper.fromJson(json['upper']) : null,\n      coverType: json['cover_type'],\n      intro: json['intro'],\n      ctime: json['ctime'],\n      mtime: json['mtime'],\n      state: json['state'],\n      favState: json['fav_state'],\n      mediaCount: json['media_count'],\n      viewCount: json['view_count'],\n      vt: json['vt'],\n      playSwitch: json['play_switch'],\n      type: json['type'],\n      link: json['link'],\n      bvid: json['bvid'],\n    );\n  }\n}\n\nclass Upper {\n  final int? mid;\n  final String? name;\n  final String? face;\n\n  Upper({\n    this.mid,\n    this.name,\n    this.face,\n  });\n\n  factory Upper.fromJson(Map<String, dynamic> json) {\n    return Upper(\n      mid: json['mid'],\n      name: json['name'],\n      face: json['face'],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/models/video/ai.dart",
    "content": "class AiConclusionModel {\n  AiConclusionModel({\n    this.code,\n    this.modelResult,\n    this.stid,\n    this.status,\n    this.likeNum,\n    this.dislikeNum,\n  });\n\n  int? code;\n  ModelResult? modelResult;\n  String? stid;\n  int? status;\n  int? likeNum;\n  int? dislikeNum;\n\n  AiConclusionModel.fromJson(Map<String, dynamic> json) {\n    code = json['code'];\n    modelResult = ModelResult.fromJson(json['model_result']);\n    stid = json['stid'];\n    status = json['status'];\n    likeNum = json['like_num'];\n    dislikeNum = json['dislike_num'];\n  }\n}\n\nclass ModelResult {\n  ModelResult({\n    this.resultType,\n    this.summary,\n    this.outline,\n  });\n\n  int? resultType;\n  String? summary;\n  List<OutlineItem>? outline;\n\n  ModelResult.fromJson(Map<String, dynamic> json) {\n    resultType = json['result_type'];\n    summary = json['summary'];\n    outline = json['result_type'] == 0\n        ? <OutlineItem>[]\n        : json['outline']\n            .map<OutlineItem>((e) => OutlineItem.fromJson(e))\n            .toList();\n  }\n}\n\nclass OutlineItem {\n  OutlineItem({\n    this.title,\n    this.partOutline,\n  });\n\n  String? title;\n  List<PartOutline>? partOutline;\n\n  OutlineItem.fromJson(Map<String, dynamic> json) {\n    title = json['title'];\n    partOutline = json['part_outline']\n        .map<PartOutline>((e) => PartOutline.fromJson(e))\n        .toList();\n  }\n}\n\nclass PartOutline {\n  PartOutline({\n    this.timestamp,\n    this.content,\n  });\n\n  int? timestamp;\n  String? content;\n\n  PartOutline.fromJson(Map<String, dynamic> json) {\n    timestamp = json['timestamp'];\n    content = json['content'];\n  }\n}\n"
  },
  {
    "path": "lib/models/video/later.dart",
    "content": "class MediaVideoItemModel {\n  MediaVideoItemModel({\n    this.id,\n    this.aid,\n    this.offset,\n    this.index,\n    this.intro,\n    this.attr,\n    this.tid,\n    this.copyRight,\n    this.cntInfo,\n    this.cover,\n    this.duration,\n    this.pubtime,\n    this.likeState,\n    this.favState,\n    this.page,\n    this.cid,\n    this.pages,\n    this.title,\n    this.type,\n    this.upper,\n    this.link,\n    this.bvid,\n    this.shortLink,\n    this.rights,\n    this.elecInfo,\n    this.coin,\n    this.progressPercent,\n    this.badge,\n    this.forbidFav,\n    this.moreType,\n    this.businessOid,\n  });\n\n  int? id;\n  int? aid;\n  int? offset;\n  int? index;\n  String? intro;\n  int? attr;\n  int? tid;\n  int? copyRight;\n  Map? cntInfo;\n  String? cover;\n  int? duration;\n  int? pubtime;\n  int? likeState;\n  int? favState;\n  int? page;\n  int? cid;\n  List<Page>? pages;\n  String? title;\n  int? type;\n  Upper? upper;\n  String? link;\n  String? bvid;\n  String? shortLink;\n  Rights? rights;\n  dynamic elecInfo;\n  Coin? coin;\n  double? progressPercent;\n  dynamic badge;\n  bool? forbidFav;\n  int? moreType;\n  int? businessOid;\n\n  factory MediaVideoItemModel.fromJson(Map<String, dynamic> json) =>\n      MediaVideoItemModel(\n        id: json[\"id\"],\n        aid: json[\"id\"],\n        offset: json[\"offset\"],\n        index: json[\"index\"],\n        intro: json[\"intro\"],\n        attr: json[\"attr\"],\n        tid: json[\"tid\"],\n        copyRight: json[\"copy_right\"],\n        cntInfo: json[\"cnt_info\"],\n        cover: json[\"cover\"],\n        duration: json[\"duration\"],\n        pubtime: json[\"pubtime\"],\n        likeState: json[\"like_state\"],\n        favState: json[\"fav_state\"],\n        page: json[\"page\"],\n        cid: json[\"pages\"] == null ? -1 : json[\"pages\"].first['id'],\n        // json[\"pages\"] 可能为null\n        pages: json[\"pages\"] == null\n            ? []\n            : List<Page>.from(json[\"pages\"].map((x) => Page.fromJson(x))),\n        title: json[\"title\"],\n        type: json[\"type\"],\n        upper: Upper.fromJson(json[\"upper\"]),\n        link: json[\"link\"],\n        bvid: json[\"bv_id\"],\n        shortLink: json[\"short_link\"],\n        rights: Rights.fromJson(json[\"rights\"]),\n        elecInfo: json[\"elec_info\"],\n        coin: Coin.fromJson(json[\"coin\"]),\n        progressPercent: json[\"progress_percent\"].toDouble(),\n        badge: json[\"badge\"],\n        forbidFav: json[\"forbid_fav\"],\n        moreType: json[\"more_type\"],\n        businessOid: json[\"business_oid\"],\n      );\n}\n\nclass Coin {\n  Coin({\n    this.maxNum,\n    this.coinNumber,\n  });\n\n  int? maxNum;\n  int? coinNumber;\n\n  factory Coin.fromJson(Map<String, dynamic> json) => Coin(\n        maxNum: json[\"max_num\"],\n        coinNumber: json[\"coin_number\"],\n      );\n}\n\nclass Page {\n  Page({\n    this.id,\n    this.title,\n    this.intro,\n    this.duration,\n    this.link,\n    this.page,\n    this.metas,\n    this.from,\n    this.dimension,\n  });\n\n  int? id;\n  String? title;\n  String? intro;\n  int? duration;\n  String? link;\n  int? page;\n  List<Meta>? metas;\n  String? from;\n  Dimension? dimension;\n\n  factory Page.fromJson(Map<String, dynamic> json) => Page(\n        id: json[\"id\"],\n        title: json[\"title\"],\n        intro: json[\"intro\"],\n        duration: json[\"duration\"],\n        link: json[\"link\"],\n        page: json[\"page\"],\n        metas: List<Meta>.from(json[\"metas\"].map((x) => Meta.fromJson(x))),\n        from: json[\"from\"],\n        dimension: Dimension.fromJson(json[\"dimension\"]),\n      );\n}\n\nclass Dimension {\n  Dimension({\n    this.width,\n    this.height,\n    this.rotate,\n  });\n\n  int? width;\n  int? height;\n  int? rotate;\n\n  factory Dimension.fromJson(Map<String, dynamic> json) => Dimension(\n        width: json[\"width\"],\n        height: json[\"height\"],\n        rotate: json[\"rotate\"],\n      );\n}\n\nclass Meta {\n  Meta({\n    this.quality,\n    this.size,\n  });\n\n  int? quality;\n  int? size;\n\n  factory Meta.fromJson(Map<String, dynamic> json) => Meta(\n        quality: json[\"quality\"],\n        size: json[\"size\"],\n      );\n}\n\nclass Rights {\n  Rights({\n    this.bp,\n    this.elec,\n    this.download,\n    this.movie,\n    this.pay,\n    this.ugcPay,\n    this.hd5,\n    this.noReprint,\n    this.autoplay,\n    this.noBackground,\n  });\n\n  int? bp;\n  int? elec;\n  int? download;\n  int? movie;\n  int? pay;\n  int? ugcPay;\n  int? hd5;\n  int? noReprint;\n  int? autoplay;\n  int? noBackground;\n\n  factory Rights.fromJson(Map<String, dynamic> json) => Rights(\n        bp: json[\"bp\"],\n        elec: json[\"elec\"],\n        download: json[\"download\"],\n        movie: json[\"movie\"],\n        pay: json[\"pay\"],\n        ugcPay: json[\"ugc_pay\"],\n        hd5: json[\"hd5\"],\n        noReprint: json[\"no_reprint\"],\n        autoplay: json[\"autoplay\"],\n        noBackground: json[\"no_background\"],\n      );\n}\n\nclass Upper {\n  Upper({\n    this.mid,\n    this.name,\n    this.face,\n    this.followed,\n    this.fans,\n    this.vipType,\n    this.vipStatue,\n    this.vipDueDate,\n    this.vipPayType,\n    this.officialRole,\n    this.officialTitle,\n    this.officialDesc,\n    this.displayName,\n  });\n\n  int? mid;\n  String? name;\n  String? face;\n  int? followed;\n  int? fans;\n  int? vipType;\n  int? vipStatue;\n  int? vipDueDate;\n  int? vipPayType;\n  int? officialRole;\n  String? officialTitle;\n  String? officialDesc;\n  String? displayName;\n\n  factory Upper.fromJson(Map<String, dynamic> json) => Upper(\n        mid: json[\"mid\"],\n        name: json[\"name\"],\n        face: json[\"face\"],\n        followed: json[\"followed\"],\n        fans: json[\"fans\"],\n        vipType: json[\"vip_type\"],\n        vipStatue: json[\"vip_statue\"],\n        vipDueDate: json[\"vip_due_date\"],\n        vipPayType: json[\"vip_pay_type\"],\n        officialRole: json[\"official_role\"],\n        officialTitle: json[\"official_title\"],\n        officialDesc: json[\"official_desc\"],\n        displayName: json[\"display_name\"],\n      );\n}\n"
  },
  {
    "path": "lib/models/video/play/ao_output.dart",
    "content": "final List aoOutputList = [\n  {'title': 'audiotrack,opensles', 'value': '0'},\n  {'title': 'opensles,audiotrack', 'value': '1'},\n  {'title': 'audiotrack', 'value': '2'},\n  {'title': 'opensles', 'value': '3'},\n];\n"
  },
  {
    "path": "lib/models/video/play/quality.dart",
    "content": "// ignore_for_file: constant_identifier_names\n\nenum VideoQuality {\n  speed240,\n  flunt360,\n  clear480,\n  high720,\n  high72060,\n  high1080,\n  high1080plus,\n  high108060,\n  super4K,\n  hdr,\n  dolbyVision,\n  super8k\n}\n\nextension VideoQualityCode on VideoQuality {\n  static final List<int> _codeList = [\n    6,\n    16,\n    32,\n    64,\n    74,\n    80,\n    112,\n    116,\n    120,\n    125,\n    126,\n    127,\n  ];\n  int get code => _codeList[index];\n\n  static VideoQuality? fromCode(int code) {\n    final index = _codeList.indexOf(code);\n    if (index != -1) {\n      return VideoQuality.values[index];\n    }\n    return null;\n  }\n\n  static int? toCode(VideoQuality quality) {\n    final index = VideoQuality.values.indexOf(quality);\n    if (index != -1 && index < _codeList.length) {\n      return _codeList[index];\n    }\n    return null;\n  }\n}\n\nextension VideoQualityDesc on VideoQuality {\n  static final List<String> _descList = [\n    '240P 极速',\n    '360P 流畅',\n    '480P 清晰',\n    '720P 高清',\n    '720P60 高帧率',\n    '1080P 高清',\n    '1080P+ 高码率',\n    '1080P60 高帧率',\n    '4K 超清',\n    'HDR 真彩色',\n    '杜比视界',\n    '8K 超高清'\n  ];\n  get description => _descList[index];\n}\n\n///\nenum AudioQuality { k64, k132, k192, dolby, hiRes }\n\nextension AudioQualityCode on AudioQuality {\n  static final List<int> _codeList = [\n    30216,\n    30232,\n    30280,\n    30250,\n    30251,\n  ];\n  int get code => _codeList[index];\n\n  static AudioQuality? fromCode(int code) {\n    final index = _codeList.indexOf(code);\n    if (index != -1) {\n      return AudioQuality.values[index];\n    }\n    return null;\n  }\n}\n\nextension AudioQualityDesc on AudioQuality {\n  static final List<String> _descList = [\n    '64K',\n    '132K',\n    '192K',\n    '杜比全景声',\n    'Hi-Res无损',\n  ];\n  get description => _descList[index];\n}\n\nenum VideoDecodeFormats {\n  DVH1,\n  AV1,\n  HEVC,\n  AVC,\n}\n\nextension VideoDecodeFormatsDesc on VideoDecodeFormats {\n  static final List<String> _descList = ['DVH1', 'AV1', 'HEVC', 'AVC'];\n  get description => _descList[index];\n}\n\nextension VideoDecodeFormatsCode on VideoDecodeFormats {\n  static final List<String> _codeList = ['dvh1', 'av01', 'hev1', 'avc1'];\n  get code => _codeList[index];\n\n  static VideoDecodeFormats? fromCode(String code) {\n    final index = _codeList.indexOf(code);\n    if (index != -1) {\n      return VideoDecodeFormats.values[index];\n    }\n    return null;\n  }\n\n  static VideoDecodeFormats? fromString(String val) {\n    var result = VideoDecodeFormats.values.first;\n    for (var i in _codeList) {\n      if (val.startsWith(i)) {\n        result = VideoDecodeFormats.values[_codeList.indexOf(i)];\n        break;\n      }\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/models/video/play/url.dart",
    "content": "import 'package:pilipala/models/video/play/quality.dart';\n\nclass PlayUrlModel {\n  PlayUrlModel({\n    this.from,\n    this.result,\n    this.message,\n    this.quality,\n    this.format,\n    this.timeLength,\n    this.acceptFormat,\n    this.acceptDesc,\n    this.acceptQuality,\n    this.videoCodecid,\n    this.seekParam,\n    this.seekType,\n    this.dash,\n    this.supportFormats,\n    // this.highFormat,\n    this.lastPlayTime,\n    this.lastPlayCid,\n  });\n\n  String? from;\n  String? result;\n  String? message;\n  int? quality;\n  String? format;\n  int? timeLength;\n  String? acceptFormat;\n  List<dynamic>? acceptDesc;\n  List<int>? acceptQuality;\n  int? videoCodecid;\n  String? seekParam;\n  String? seekType;\n  Dash? dash;\n  List<Durl>? durl;\n  List<FormatItem>? supportFormats;\n  // String? highFormat;\n  int? lastPlayTime;\n  int? lastPlayCid;\n\n  PlayUrlModel.fromJson(Map<String, dynamic> json) {\n    from = json['from'];\n    result = json['result'];\n    message = json['message'];\n    quality = json['quality'];\n    format = json['format'];\n    timeLength = json['timelength'];\n    acceptFormat = json['accept_format'];\n    acceptDesc = json['accept_description'];\n    acceptQuality = json['accept_quality'].map<int>((e) => e as int).toList();\n    videoCodecid = json['video_codecid'];\n    seekParam = json['seek_param'];\n    seekType = json['seek_type'];\n    dash = json['dash'] != null ? Dash.fromJson(json['dash']) : null;\n    durl = json['durl']?.map<Durl>((e) => Durl.fromJson(e)).toList();\n    supportFormats = json['support_formats'] != null\n        ? json['support_formats']\n            .map<FormatItem>((e) => FormatItem.fromJson(e))\n            .toList()\n        : [];\n    lastPlayTime = json['last_play_time'];\n    lastPlayCid = json['last_play_cid'];\n  }\n}\n\nclass Dash {\n  Dash({\n    this.duration,\n    this.minBufferTime,\n    this.video,\n    this.audio,\n    this.dolby,\n    this.flac,\n  });\n\n  int? duration;\n  double? minBufferTime;\n  List<VideoItem>? video;\n  List<AudioItem>? audio;\n  Dolby? dolby;\n  Flac? flac;\n\n  Dash.fromJson(Map<String, dynamic> json) {\n    duration = json['duration'];\n    minBufferTime = json['minBufferTime'];\n    video = json['video'].map<VideoItem>((e) => VideoItem.fromJson(e)).toList();\n    audio = json['audio'] != null\n        ? json['audio'].map<AudioItem>((e) => AudioItem.fromJson(e)).toList()\n        : [];\n    dolby = json['dolby'] != null ? Dolby.fromJson(json['dolby']) : null;\n    flac = json['flac'] != null ? Flac.fromJson(json['flac']) : null;\n  }\n}\n\nclass VideoItem {\n  VideoItem({\n    this.id,\n    this.baseUrl,\n    this.backupUrl,\n    this.bandWidth,\n    this.mimeType,\n    this.codecs,\n    this.width,\n    this.height,\n    this.frameRate,\n    this.sar,\n    this.startWithSap,\n    this.segmentBase,\n    this.codecid,\n    this.quality,\n  });\n\n  int? id;\n  String? baseUrl;\n  String? backupUrl;\n  int? bandWidth;\n  String? mimeType;\n  String? codecs;\n  int? width;\n  int? height;\n  String? frameRate;\n  String? sar;\n  int? startWithSap;\n  Map? segmentBase;\n  int? codecid;\n  VideoQuality? quality;\n\n  VideoItem.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    baseUrl = json['baseUrl'];\n    backupUrl =\n        json['backupUrl'] != null ? json['backupUrl'].toList().first : '';\n    bandWidth = json['bandWidth'];\n    mimeType = json['mime_type'];\n    codecs = json['codecs'];\n    width = json['width'];\n    height = json['height'];\n    frameRate = json['frameRate'];\n    sar = json['sar'];\n    startWithSap = json['startWithSap'];\n    segmentBase = json['segmentBase'];\n    codecid = json['codecid'];\n    quality = VideoQuality.values.firstWhere((i) => i.code == json['id']);\n  }\n}\n\nclass AudioItem {\n  AudioItem({\n    this.id,\n    this.baseUrl,\n    this.backupUrl,\n    this.bandWidth,\n    this.mimeType,\n    this.codecs,\n    this.width,\n    this.height,\n    this.frameRate,\n    this.sar,\n    this.startWithSap,\n    this.segmentBase,\n    this.codecid,\n    this.quality,\n  });\n\n  int? id;\n  String? baseUrl;\n  String? backupUrl;\n  int? bandWidth;\n  String? mimeType;\n  String? codecs;\n  int? width;\n  int? height;\n  String? frameRate;\n  String? sar;\n  int? startWithSap;\n  Map? segmentBase;\n  int? codecid;\n  String? quality;\n\n  AudioItem.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    baseUrl = json['baseUrl'];\n    backupUrl =\n        json['backupUrl'] != null ? json['backupUrl'].toList().first : '';\n    bandWidth = json['bandWidth'];\n    mimeType = json['mime_type'];\n    codecs = json['codecs'];\n    width = json['width'];\n    height = json['height'];\n    frameRate = json['frameRate'];\n    sar = json['sar'];\n    startWithSap = json['startWithSap'];\n    segmentBase = json['segmentBase'];\n    codecid = json['codecid'];\n    quality =\n        AudioQuality.values.firstWhere((i) => i.code == json['id']).description;\n  }\n}\n\nclass FormatItem {\n  FormatItem({\n    this.quality,\n    this.format,\n    this.newDesc,\n    this.displayDesc,\n    this.codecs,\n  });\n\n  int? quality;\n  String? format;\n  String? newDesc;\n  String? displayDesc;\n  List? codecs;\n\n  FormatItem.fromJson(Map<String, dynamic> json) {\n    quality = json['quality'];\n    format = json['format'];\n    newDesc = json['new_description'];\n    displayDesc = json['display_desc'];\n    codecs = json['codecs'];\n  }\n}\n\nclass Dolby {\n  Dolby({\n    this.type,\n    this.audio,\n  });\n\n  // 1：普通杜比音效 2：全景杜比音效\n  int? type;\n  List<AudioItem>? audio;\n\n  Dolby.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    audio = json['audio'] != null\n        ? json['audio'].map<AudioItem>((e) => AudioItem.fromJson(e)).toList()\n        : [];\n  }\n}\n\nclass Flac {\n  Flac({this.display, this.audio});\n\n  bool? display;\n  AudioItem? audio;\n\n  Flac.fromJson(Map<String, dynamic> json) {\n    display = json['display'];\n    audio = json['audio'] != null ? AudioItem.fromJson(json['audio']) : null;\n  }\n}\n\nclass Durl {\n  Durl({\n    this.order,\n    this.length,\n    this.size,\n    this.ahead,\n    this.vhead,\n    this.url,\n  });\n\n  int? order;\n  int? length;\n  int? size;\n  String? ahead;\n  String? vhead;\n  String? url;\n\n  Durl.fromJson(Map<String, dynamic> json) {\n    order = json['order'];\n    length = json['length'];\n    size = json['size'];\n    ahead = json['ahead'];\n    vhead = json['vhead'];\n    url = json['url'];\n  }\n}\n"
  },
  {
    "path": "lib/models/video/reply/config.dart",
    "content": "class ReplyConfig {\n  ReplyConfig({\n    this.showtopic,\n    this.showUpFlag,\n    this.readOnly,\n  });\n\n  int? showtopic;\n  bool? showUpFlag;\n  bool? readOnly;\n\n  ReplyConfig.fromJson(Map<String, dynamic> json) {\n    showtopic = json['showtopic'];\n    showUpFlag = json['show_up_flag'];\n    readOnly = json['read_only'];\n  }\n}\n"
  },
  {
    "path": "lib/models/video/reply/content.dart",
    "content": "class ReplyContent {\n  ReplyContent({\n    this.message,\n    this.atNameToMid, // @的用户的mid null\n    this.members, // 被@的用户List 如果有的话 []\n    this.emote, // 表情包 如果有的话 null\n    this.jumpUrl, // {}\n    this.pictures, // {}\n    this.vote,\n    this.richText,\n    this.isText,\n    this.topicsMeta,\n  });\n\n  String? message;\n  Map? atNameToMid;\n  List<MemberItemModel>? members;\n  Map? emote;\n  Map? jumpUrl;\n  List? pictures;\n  Map? vote;\n  Map? richText;\n  bool? isText;\n  Map? topicsMeta;\n\n  ReplyContent.fromJson(Map<String, dynamic> json) {\n    message = json['message']\n        .replaceAll('&gt;', '>')\n        .replaceAll('&#34;', '\"')\n        .replaceAll('&#39;', \"'\");\n    atNameToMid = json['at_name_to_mid'] ?? {};\n    members = json['members'] != null\n        ? json['members']\n            .map<MemberItemModel>((e) => MemberItemModel.fromJson(e))\n            .toList()\n        : [];\n    emote = json['emote'] ?? {};\n    jumpUrl = json['jump_url'] ?? {};\n    pictures = json['pictures'] ?? [];\n    vote = json['vote'] ?? {};\n    richText = json['rich_text'] ?? {};\n    // 不包含@ 笔记 图片的时候，文字可折叠\n    isText = atNameToMid!.isEmpty && vote!.isEmpty && pictures!.isEmpty;\n    topicsMeta = json['topics_meta'] ?? {};\n  }\n}\n\nclass MemberItemModel {\n  MemberItemModel({\n    required this.mid,\n    required this.uname,\n  });\n\n  late String mid;\n  late String uname;\n\n  MemberItemModel.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    uname = json['uname'];\n  }\n}\n"
  },
  {
    "path": "lib/models/video/reply/data.dart",
    "content": "import 'package:pilipala/models/video/reply/item.dart';\n\nimport 'config.dart';\nimport 'page.dart';\nimport 'upper.dart';\n\nclass ReplyData {\n  ReplyData({\n    this.page,\n    this.config,\n    this.replies,\n    this.topReplies,\n    this.upper,\n  });\n\n  ReplyPage? page;\n  ReplyConfig? config;\n  late List<ReplyItemModel>? replies;\n  late List<ReplyItemModel>? topReplies;\n  ReplyUpper? upper;\n\n  ReplyData.fromJson(Map<String, dynamic> json) {\n    page = ReplyPage.fromJson(json['page']);\n    config = ReplyConfig.fromJson(json['config']);\n    replies = json['replies'] != null\n        ? json['replies']\n            .map<ReplyItemModel>(\n                (item) => ReplyItemModel.fromJson(item, json['upper']['mid']))\n            .toList()\n        : [];\n    topReplies = json['top_replies'] != null\n        ? json['top_replies']\n            .map<ReplyItemModel>((item) => ReplyItemModel.fromJson(\n                item, json['upper']['mid'],\n                isTopStatus: true))\n            .toList()\n        : [];\n    upper = ReplyUpper.fromJson(json['upper']);\n  }\n}\n"
  },
  {
    "path": "lib/models/video/reply/emote.dart",
    "content": "class EmoteModelData {\n  final List<PackageItem>? packages;\n\n  EmoteModelData({\n    required this.packages,\n  });\n\n  factory EmoteModelData.fromJson(Map<String, dynamic> jsonRes) {\n    final List<PackageItem>? packages =\n        jsonRes['packages'] is List ? <PackageItem>[] : null;\n    if (packages != null) {\n      for (final dynamic item in jsonRes['packages']!) {\n        if (item != null) {\n          try {\n            packages.add(PackageItem.fromJson(item));\n          } catch (_) {}\n        }\n      }\n    }\n    return EmoteModelData(\n      packages: packages,\n    );\n  }\n}\n\nclass PackageItem {\n  final int? id;\n  final String? text;\n  final String? url;\n  final int? mtime;\n  final int? type;\n  final int? attr;\n  final Meta? meta;\n  final List<Emote>? emote;\n\n  PackageItem({\n    required this.id,\n    required this.text,\n    required this.url,\n    required this.mtime,\n    required this.type,\n    required this.attr,\n    required this.meta,\n    required this.emote,\n  });\n\n  factory PackageItem.fromJson(Map<String, dynamic> jsonRes) {\n    final List<Emote>? emote = jsonRes['emote'] is List ? <Emote>[] : null;\n    if (emote != null) {\n      for (final dynamic item in jsonRes['emote']!) {\n        if (item != null) {\n          try {\n            emote.add(Emote.fromJson(item));\n          } catch (_) {}\n        }\n      }\n    }\n    return PackageItem(\n      id: jsonRes['id'],\n      text: jsonRes['text'],\n      url: jsonRes['url'],\n      mtime: jsonRes['mtime'],\n      type: jsonRes['type'],\n      attr: jsonRes['attr'],\n      meta: Meta.fromJson(jsonRes['meta']),\n      emote: emote,\n    );\n  }\n}\n\nclass Meta {\n  final int? size;\n  final List<String>? suggest;\n\n  Meta({\n    required this.size,\n    required this.suggest,\n  });\n\n  factory Meta.fromJson(Map<String, dynamic> jsonRes) => Meta(\n        size: jsonRes['size'],\n        suggest: jsonRes['suggest'] is List ? <String>[] : null,\n      );\n}\n\nclass Emote {\n  final int? id;\n  final int? packageId;\n  final String? text;\n  final String? url;\n  final int? mtime;\n  final int? type;\n  final int? attr;\n  final Meta? meta;\n  final dynamic activity;\n\n  Emote({\n    required this.id,\n    required this.packageId,\n    required this.text,\n    required this.url,\n    required this.mtime,\n    required this.type,\n    required this.attr,\n    required this.meta,\n    required this.activity,\n  });\n\n  factory Emote.fromJson(Map<String, dynamic> jsonRes) => Emote(\n        id: jsonRes['id'],\n        packageId: jsonRes['package_id'],\n        text: jsonRes['text'],\n        url: jsonRes['url'],\n        mtime: jsonRes['mtime'],\n        type: jsonRes['type'],\n        attr: jsonRes['attr'],\n        meta: Meta.fromJson(jsonRes['meta']),\n        activity: jsonRes['activity'],\n      );\n}\n"
  },
  {
    "path": "lib/models/video/reply/item.dart",
    "content": "import 'content.dart';\nimport 'member.dart';\n\nclass ReplyItemModel {\n  ReplyItemModel({\n    this.rpid,\n    this.oid,\n    this.type,\n    this.mid,\n    this.root,\n    this.parent,\n    this.dialog,\n    this.count,\n    this.floor,\n    this.state,\n    this.fansgrade,\n    this.attr,\n    this.ctime,\n    this.rpidStr,\n    this.rootStr,\n    this.parentStr,\n    this.like,\n    this.action,\n    this.member,\n    this.content,\n    this.replies,\n    this.assist,\n    this.upAction,\n    this.invisible,\n    this.replyControl,\n    this.isUp,\n    this.isTop,\n    this.cardLabel,\n  });\n\n  int? rpid;\n  int? oid;\n  int? type;\n  int? mid;\n  int? root;\n  int? parent;\n  int? dialog;\n  int? count;\n  int? floor;\n  int? state;\n  int? fansgrade;\n  int? attr;\n  int? ctime;\n  String? rpidStr;\n  String? rootStr;\n  String? parentStr;\n  int? like;\n  int? action;\n  ReplyMember? member;\n  ReplyContent? content;\n  List? replies;\n  int? assist;\n  UpAction? upAction;\n  bool? invisible;\n  ReplyControl? replyControl;\n  bool? isUp;\n  bool? isTop = false;\n  List? cardLabel;\n\n  ReplyItemModel.fromJson(Map<String, dynamic> json, upperMid,\n      {isTopStatus = false}) {\n    rpid = json['rpid'];\n    oid = json['oid'];\n    type = json['type'];\n    mid = json['mid'];\n    root = json['root'];\n    parent = json['parent'];\n    dialog = json['dialog'];\n    count = json['count'];\n    floor = json['floor'];\n    state = json['state'];\n    fansgrade = json['fansgrade'];\n    attr = json['attr'];\n    ctime = json['ctime'];\n    rpidStr = json['rpid_str'];\n    rootStr = json['root_str'];\n    parentStr = json['parent_str'];\n    like = json['like'];\n    action = json['action'];\n    member = ReplyMember.fromJson(json['member']);\n    content = ReplyContent.fromJson(json['content']);\n    replies = json['replies'] != null\n        ? json['replies']\n            .map((item) => ReplyItemModel.fromJson(item, upperMid))\n            .toList()\n        : [];\n    assist = json['assist'];\n    upAction = UpAction.fromJson(json['up_action']);\n    invisible = json['invisible'];\n    replyControl = json['reply_control'] == null\n        ? null\n        : ReplyControl.fromJson(json['reply_control']);\n    isUp = upperMid.toString() == json['member']['mid'];\n    isTop = isTopStatus;\n    cardLabel = json['card_label'] != null\n        ? json['card_label'].map((e) => e['text_content']).toList()\n        : [];\n  }\n}\n\nclass UpAction {\n  UpAction({this.like, this.reply});\n\n  bool? like;\n  bool? reply;\n\n  UpAction.fromJson(Map<String, dynamic> json) {\n    like = json['like'];\n    reply = json['reply'];\n  }\n}\n\nclass ReplyControl {\n  ReplyControl({\n    this.upReply,\n    this.isUpTop,\n    this.upLike,\n    this.isShow,\n    this.entryText,\n    this.titleText,\n    this.time,\n    this.location,\n  });\n\n  bool? upReply;\n  bool? isUpTop;\n  bool? upLike;\n  bool? isShow;\n  String? entryText;\n  String? titleText;\n  String? time;\n  String? location;\n\n  ReplyControl.fromJson(Map<String, dynamic> json) {\n    upReply = json['up_reply'] ?? false;\n    isUpTop = json['is_up_top'] ?? false;\n    upLike = json['up_like'] ?? false;\n    if (json['sub_reply_entry_text'] != null) {\n      final RegExp regex = RegExp(r\"\\d+\");\n      final RegExpMatch match = regex.firstMatch(\n          json['sub_reply_entry_text'] == null\n              ? ''\n              : json['sub_reply_entry_text']!)!;\n      isShow = int.parse(match.group(0)!) >= 3;\n    } else {\n      isShow = false;\n    }\n\n    entryText = json['sub_reply_entry_text'];\n    titleText = json['sub_reply_title_text'];\n    time = json['time_desc'];\n    location = json['location'] != null ? json['location'].split('：')[1] : '';\n  }\n}\n"
  },
  {
    "path": "lib/models/video/reply/member.dart",
    "content": "class ReplyMember {\n  ReplyMember({\n    this.mid,\n    this.uname,\n    this.sign,\n    this.avatar,\n    this.level,\n    this.pendant,\n    this.officialVerify,\n    this.vip,\n    this.fansDetail,\n  });\n\n  String? mid;\n  String? uname;\n  String? sign;\n  String? avatar;\n  int? level;\n  Pendant? pendant;\n  Map? officialVerify;\n  Map? vip;\n  Map? fansDetail;\n  UserSailing? userSailing;\n\n  ReplyMember.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    uname = json['uname'];\n    sign = json['sign'];\n    avatar = json['avatar'];\n    level = json['level_info']['current_level'];\n    pendant = Pendant.fromJson(json['pendant']);\n    officialVerify = json['officia_verify'];\n    vip = json['vip'];\n    fansDetail = json['fans_detail'];\n    userSailing = json['user_sailing'] != null\n        ? UserSailing.fromJson(json['user_sailing'])\n        : UserSailing();\n  }\n}\n\nclass Pendant {\n  Pendant({\n    this.pid,\n    this.name,\n    this.image,\n  });\n\n  int? pid;\n  String? name;\n  String? image;\n\n  Pendant.fromJson(Map<String, dynamic> json) {\n    pid = json['pid'];\n    name = json['name'];\n    image = json['image'];\n  }\n}\n\nclass UserSailing {\n  UserSailing({this.pendant, this.cardbg});\n\n  Map? pendant;\n  Map? cardbg;\n\n  UserSailing.fromJson(Map<String, dynamic> json) {\n    pendant = json['pendant'];\n    cardbg = json['cardbg'];\n  }\n}\n"
  },
  {
    "path": "lib/models/video/reply/page.dart",
    "content": "class ReplyPage {\n  ReplyPage({\n    this.num,\n    this.size,\n    this.count,\n    this.acount,\n  });\n\n  int? num;\n  int? size;\n  int? count;\n  int? acount;\n\n  ReplyPage.fromJson(Map<String, dynamic> json) {\n    num = json['num'];\n    size = json['size'];\n    count = json['count'];\n    acount = json['acount'];\n  }\n}\n"
  },
  {
    "path": "lib/models/video/reply/top_replies.dart",
    "content": "class ReplyTop {}\n"
  },
  {
    "path": "lib/models/video/reply/upper.dart",
    "content": "import 'item.dart';\n\nclass ReplyUpper {\n  ReplyUpper({\n    this.mid,\n    this.top,\n  });\n\n  int? mid;\n  ReplyItemModel? top;\n\n  ReplyUpper.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    top = json['top'] != null\n        ? ReplyItemModel.fromJson(json['top'], json['mid'])\n        : null;\n  }\n}\n"
  },
  {
    "path": "lib/models/video/subTitile/content.dart",
    "content": "class SubTitileContentModel {\n  double? from;\n  double? to;\n  int? location;\n  String? content;\n\n  SubTitileContentModel({\n    this.from,\n    this.to,\n    this.location,\n    this.content,\n  });\n\n  SubTitileContentModel.fromJson(Map<String, dynamic> json) {\n    from = json['from'];\n    to = json['to'];\n    location = json['location'];\n    content = json['content'];\n  }\n}\n"
  },
  {
    "path": "lib/models/video/subTitile/result.dart",
    "content": "class SubTitlteModel {\n  SubTitlteModel({\n    this.aid,\n    this.bvid,\n    this.cid,\n    this.loginMid,\n    this.loginMidHash,\n    this.isOwner,\n    this.name,\n    this.subtitles,\n  });\n\n  int? aid;\n  String? bvid;\n  int? cid;\n  int? loginMid;\n  String? loginMidHash;\n  bool? isOwner;\n  String? name;\n  List<SubTitlteItemModel>? subtitles;\n\n  factory SubTitlteModel.fromJson(Map<String, dynamic> json) => SubTitlteModel(\n        aid: json[\"aid\"],\n        bvid: json[\"bvid\"],\n        cid: json[\"cid\"],\n        loginMid: json[\"login_mid\"],\n        loginMidHash: json[\"login_mid_hash\"],\n        isOwner: json[\"is_owner\"],\n        name: json[\"name\"],\n        subtitles: json[\"subtitle\"] != null\n            ? json[\"subtitle\"][\"subtitles\"]\n                .map<SubTitlteItemModel>((x) => SubTitlteItemModel.fromJson(x))\n                .toList()\n            : [],\n      );\n}\n\nclass SubTitlteItemModel {\n  SubTitlteItemModel({\n    this.id,\n    this.lan,\n    this.lanDoc,\n    this.isLock,\n    this.subtitleUrl,\n    this.type,\n    this.aiType,\n    this.aiStatus,\n    this.title,\n    this.code,\n    this.content,\n    this.body,\n  });\n\n  int? id;\n  String? lan;\n  String? lanDoc;\n  bool? isLock;\n  String? subtitleUrl;\n  int? type;\n  int? aiType;\n  int? aiStatus;\n  String? title;\n  int? code;\n  String? content;\n  List? body;\n\n  factory SubTitlteItemModel.fromJson(Map<String, dynamic> json) =>\n      SubTitlteItemModel(\n        id: json[\"id\"],\n        lan: json[\"lan\"].replaceAll('-', ''),\n        lanDoc: json[\"lan_doc\"],\n        isLock: json[\"is_lock\"],\n        subtitleUrl: json[\"subtitle_url\"],\n        type: json[\"type\"],\n        aiType: json[\"ai_type\"],\n        aiStatus: json[\"ai_status\"],\n        title: json[\"lan_doc\"],\n        content: '',\n        body: [],\n      );\n}\n"
  },
  {
    "path": "lib/models/video_detail_res.dart",
    "content": "import 'dart:convert';\n\nclass VideoDetailResponse {\n  int? code;\n  String? message;\n  int? ttl;\n  VideoDetailData? data;\n\n  VideoDetailResponse({\n    this.code,\n    this.message,\n    this.ttl,\n    this.data,\n  });\n\n  VideoDetailResponse.fromJson(Map<String, dynamic> json) {\n    code = json[\"code\"];\n    message = json[\"message\"];\n    ttl = json[\"ttl\"];\n    data = json[\"data\"] == null ? null : VideoDetailData.fromJson(json[\"data\"]);\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n    data[\"code\"] = code;\n    data[\"message\"] = message;\n    data[\"ttl\"] = ttl;\n    data[\"data\"] = data;\n\n    return data;\n  }\n}\n\nclass VideoDetailData {\n  String? bvid;\n  int? aid;\n  int? videos;\n  int? tid;\n  String? tname;\n  int? copyright;\n  String? pic;\n  String? title;\n  int? pubdate;\n  int? ctime;\n  String? desc;\n  List<DescV2>? descV2;\n  int? state;\n  int? duration;\n  Map<String, int>? rights;\n  Owner? owner;\n  Stat? stat;\n  String? videoDynamic;\n  int? cid;\n  Dimension? dimension;\n  dynamic premiere;\n  int? teenageMode;\n  bool? isChargeableSeason;\n  bool? isStory;\n  bool? noCache;\n  List<Part>? pages;\n  Subtitle? subtitle;\n  // Label? label;\n  UgcSeason? ugcSeason;\n  bool? isSeasonDisplay;\n  UserGarb? userGarb;\n  HonorReply? honorReply;\n  String? likeIcon;\n  bool? needJumpBv;\n  String? epId;\n  List<Staff>? staff;\n\n  VideoDetailData({\n    this.bvid,\n    this.aid,\n    this.videos,\n    this.tid,\n    this.tname,\n    this.copyright,\n    this.pic,\n    this.title,\n    this.pubdate,\n    this.ctime,\n    this.desc,\n    this.descV2,\n    this.state,\n    this.duration,\n    this.rights,\n    this.owner,\n    this.stat,\n    this.videoDynamic,\n    this.cid,\n    this.dimension,\n    this.premiere,\n    this.teenageMode,\n    this.isChargeableSeason,\n    this.isStory,\n    this.noCache,\n    this.pages,\n    this.subtitle,\n    this.ugcSeason,\n    this.isSeasonDisplay,\n    this.userGarb,\n    this.honorReply,\n    this.likeIcon,\n    this.needJumpBv,\n    this.epId,\n    this.staff,\n  });\n\n  VideoDetailData.fromJson(Map<String, dynamic> json) {\n    bvid = json[\"bvid\"];\n    aid = json[\"aid\"];\n    videos = json[\"videos\"];\n    tid = json[\"tid\"];\n    tname = json[\"tname\"];\n    copyright = json[\"copyright\"];\n    pic = json[\"pic\"];\n    title = json[\"title\"];\n    pubdate = json[\"pubdate\"];\n    ctime = json[\"ctime\"];\n    desc = json[\"desc\"];\n    descV2 = json[\"desc_v2\"] == null\n        ? []\n        : List<DescV2>.from(json[\"desc_v2\"]!.map((e) => DescV2.fromJson(e)));\n    state = json[\"state\"];\n    duration = json[\"duration\"];\n    rights =\n        Map.from(json[\"rights\"]!).map((k, v) => MapEntry<String, int>(k, v));\n    owner = json[\"owner\"] == null ? null : Owner.fromJson(json[\"owner\"]);\n    stat = json[\"stat\"] == null ? null : Stat.fromJson(json[\"stat\"]);\n    videoDynamic = json[\"dynamic\"];\n    cid = json[\"cid\"];\n    dimension = json[\"dimension\"] == null\n        ? null\n        : Dimension.fromJson(json[\"dimension\"]);\n    premiere = json[\"premiere\"];\n    teenageMode = json[\"teenage_mode\"];\n    isChargeableSeason = json[\"is_chargeable_season\"];\n    isStory = json[\"is_story\"];\n    noCache = json[\"no_cache\"];\n    pages = json[\"pages\"] == null\n        ? []\n        : List<Part>.from(json[\"pages\"]!.map((e) => Part.fromJson(e)));\n    subtitle =\n        json[\"subtitle\"] == null ? null : Subtitle.fromJson(json[\"subtitle\"]);\n    ugcSeason = json[\"ugc_season\"] != null\n        ? UgcSeason.fromJson(json[\"ugc_season\"])\n        : null;\n    isSeasonDisplay = json[\"is_season_display\"];\n    userGarb =\n        json[\"user_garb\"] == null ? null : UserGarb.fromJson(json[\"user_garb\"]);\n    honorReply = json[\"honor_reply\"] == null\n        ? null\n        : HonorReply.fromJson(json[\"honor_reply\"]);\n    likeIcon = json[\"like_icon\"];\n    needJumpBv = json[\"need_jump_bv\"];\n    if (json['redirect_url'] != null) {\n      epId = resolveEpId(json['redirect_url']);\n    }\n    staff = json[\"staff\"] != null\n        ? List<Staff>.from(json[\"staff\"]!.map((e) => Staff.fromJson(e)))\n        : null;\n  }\n\n  String resolveEpId(url) {\n    RegExp regex = RegExp(r'\\d+');\n    Iterable<Match> matches = regex.allMatches(url);\n    List<String> numbers = [];\n    for (Match match in matches) {\n      numbers.add(match.group(0)!);\n    }\n    return numbers[0];\n  }\n\n  Map<String, dynamic> toJson() => {\n        \"bvid\": bvid,\n        \"aid\": aid,\n        \"videos\": videos,\n        \"tid\": tid,\n        \"tname\": tname,\n        \"copyright\": copyright,\n        \"pic\": pic,\n        \"title\": title,\n        \"pubdate\": pubdate,\n        \"ctime\": ctime,\n        \"desc\": desc,\n        \"desc_v2\": descV2 == null\n            ? []\n            : List<dynamic>.from(descV2!.map((e) => e.toJson())),\n        \"state\": state,\n        \"duration\": duration,\n        \"rights\":\n            Map.from(rights!).map((k, v) => MapEntry<String, dynamic>(k, v)),\n        \"owner\": owner?.toJson(),\n        \"stat\": stat?.toJson(),\n        \"dynamic\": videoDynamic,\n        \"cid\": cid,\n        \"dimension\": dimension?.toJson(),\n        \"premiere\": premiere,\n        \"teenage_mode\": teenageMode,\n        \"is_chargeable_season\": isChargeableSeason,\n        \"is_story\": isStory,\n        \"no_cache\": noCache,\n        \"pages\": pages == null\n            ? []\n            : List<dynamic>.from(pages!.map((e) => e.toJson())),\n        \"subtitle\": subtitle?.toJson(),\n        \"is_season_display\": isSeasonDisplay,\n        \"user_garb\": userGarb?.toJson(),\n        \"honor_reply\": honorReply?.toJson(),\n        \"like_icon\": likeIcon,\n        \"need_jump_bv\": needJumpBv,\n      };\n}\n\nclass DescV2 {\n  String? rawText;\n  int? type;\n  int? bizId;\n\n  DescV2({\n    this.rawText,\n    this.type,\n    this.bizId,\n  });\n\n  fromRawJson(String str) {\n    return DescV2.fromJson(json.decode(str));\n  }\n\n  String toRawJson() => json.encode(toJson());\n\n  DescV2.fromJson(Map<String, dynamic> json) {\n    rawText = json[\"raw_text\"];\n    type = json[\"type\"];\n    bizId = json[\"biz_id\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n\n    data[\"raw_text\"] = rawText;\n    data[\"type\"] = type;\n    data[\"biz_id\"] = bizId;\n\n    return data;\n  }\n}\n\nclass Dimension {\n  int? width;\n  int? height;\n  int? rotate;\n\n  Dimension({\n    this.width,\n    this.height,\n    this.rotate,\n  });\n\n  fromRawJson(String str) => Dimension.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  Dimension.fromJson(Map<String, dynamic> json) {\n    width = json[\"width\"];\n    height = json[\"height\"];\n    rotate = json[\"rotate\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n\n    data[\"width\"] = width;\n    data[\"height\"] = height;\n    data[\"rotate\"] = rotate;\n    data[\"data\"] = data;\n\n    return data;\n  }\n}\n\nclass HonorReply {\n  List<Honor>? honor;\n\n  HonorReply({\n    this.honor,\n  });\n\n  fromRawJson(String str) => HonorReply.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  HonorReply.fromJson(Map<String, dynamic> json) {\n    honor = json[\"honor\"] == null\n        ? []\n        : List<Honor>.from(json[\"honor\"]!.map((x) => Honor.fromJson(x)));\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n\n    data[\"honor\"] =\n        honor == null ? [] : List<dynamic>.from(honor!.map((x) => x.toJson()));\n    return data;\n  }\n}\n\nclass Honor {\n  int? aid;\n  int? type;\n  String? desc;\n  int? weeklyRecommendNum;\n\n  Honor({\n    this.aid,\n    this.type,\n    this.desc,\n    this.weeklyRecommendNum,\n  });\n\n  fromRawJson(String str) => Honor.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  Honor.fromJson(Map<String, dynamic> json) {\n    aid = json[\"aid\"];\n    type = json[\"type\"];\n    desc = json[\"desc\"];\n    weeklyRecommendNum = json[\"weekly_recommend_num\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n\n    data[\"aid\"] = aid;\n    data[\"type\"] = type;\n    data[\"desc\"] = desc;\n    data[\"weekly_recommend_num\"] = weeklyRecommendNum;\n\n    return data;\n  }\n}\n\nclass Owner {\n  int? mid;\n  String? name;\n  String? face;\n\n  Owner({\n    this.mid,\n    this.name,\n    this.face,\n  });\n\n  fromRawJson(String str) => Owner.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  Owner.fromJson(Map<String, dynamic> json) {\n    mid = json[\"mid\"];\n    name = json[\"name\"];\n    face = json[\"face\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n    data[\"mid\"] = mid;\n    data[\"name\"] = name;\n    data[\"face\"] = face;\n    return data;\n  }\n}\n\nclass Part {\n  int? cid;\n  int? page;\n  String? from;\n  String? pagePart;\n  int? duration;\n  String? vid;\n  String? weblink;\n  Dimension? dimension;\n  String? firstFrame;\n  String? cover;\n\n  Part({\n    this.cid,\n    this.page,\n    this.from,\n    this.pagePart,\n    this.duration,\n    this.vid,\n    this.weblink,\n    this.dimension,\n    this.firstFrame,\n    this.cover,\n  });\n\n  fromRawJson(String str) => Part.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  Part.fromJson(Map<String, dynamic> json) {\n    cid = json[\"cid\"];\n    page = json[\"page\"];\n    from = json[\"from\"];\n    pagePart = json[\"part\"];\n    duration = json[\"duration\"];\n    vid = json[\"vid\"];\n    weblink = json[\"weblink\"];\n    dimension = json[\"dimension\"] == null\n        ? null\n        : Dimension.fromJson(json[\"dimension\"]);\n    firstFrame = json[\"first_frame\"] ?? '';\n    cover = json[\"first_frame\"] ?? '';\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n    data[\"cid\"] = cid;\n    data[\"page\"] = page;\n    data[\"from\"] = from;\n    data[\"part\"] = pagePart;\n    data[\"duration\"] = duration;\n    data[\"vid\"] = vid;\n    data[\"weblink\"] = weblink;\n    data[\"dimension\"] = dimension?.toJson();\n    data[\"first_frame\"] = firstFrame;\n    return data;\n  }\n}\n\nclass Stat {\n  int? aid;\n  int? view;\n  int? danmaku;\n  int? reply;\n  int? favorite;\n  int? coin;\n  int? share;\n  int? nowRank;\n  int? hisRank;\n  int? like;\n  int? dislike;\n  String? evaluation;\n  String? argueMsg;\n\n  Stat({\n    this.aid,\n    this.view,\n    this.danmaku,\n    this.reply,\n    this.favorite,\n    this.coin,\n    this.share,\n    this.nowRank,\n    this.hisRank,\n    this.like,\n    this.dislike,\n    this.evaluation,\n    this.argueMsg,\n  });\n\n  fromRawJson(String str) => Stat.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  Stat.fromJson(Map<String, dynamic> json) {\n    aid = json[\"aid\"];\n    view = json[\"view\"];\n    danmaku = json[\"danmaku\"];\n    reply = json[\"reply\"];\n    favorite = json[\"favorite\"];\n    coin = json[\"coin\"];\n    share = json[\"share\"];\n    nowRank = json[\"now_rank\"];\n    hisRank = json[\"his_rank\"];\n    like = json[\"like\"];\n    dislike = json[\"dislike\"];\n    evaluation = json[\"evaluation\"];\n    argueMsg = json[\"argue_msg\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n\n    data[\"aid\"] = aid;\n    data[\"view\"] = view;\n    data[\"danmaku\"] = danmaku;\n    data[\"reply\"] = reply;\n    data[\"favorite\"] = favorite;\n    data[\"coin\"] = coin;\n    data[\"share\"] = share;\n    data[\"now_rank\"] = nowRank;\n    data[\"his_rank\"] = hisRank;\n    data[\"like\"] = like;\n    data[\"dislike\"] = dislike;\n    data[\"evaluation\"] = evaluation;\n    data[\"argue_msg\"] = argueMsg;\n    return data;\n  }\n}\n\nclass Subtitle {\n  bool? allowSubmit;\n  List<dynamic>? list;\n\n  Subtitle({\n    this.allowSubmit,\n    this.list,\n  });\n\n  fromRawJson(String str) => Subtitle.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  Subtitle.fromJson(Map<String, dynamic> json) {\n    allowSubmit = json[\"allow_submit\"];\n    list = json[\"list\"] == null\n        ? []\n        : List<dynamic>.from(json[\"list\"]!.map((x) => x));\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n\n    data[\"allow_submit\"] = allowSubmit;\n    data[\"list\"] = list == null ? [] : List<dynamic>.from(list!.map((x) => x));\n    return data;\n  }\n}\n\nclass UserGarb {\n  String? urlImageAniCut;\n\n  UserGarb({\n    this.urlImageAniCut,\n  });\n\n  fromRawJson(String str) => UserGarb.fromJson(json.decode(str));\n\n  String toRawJson() => json.encode(toJson());\n\n  UserGarb.fromJson(Map<String, dynamic> json) {\n    urlImageAniCut = json[\"url_image_ani_cut\"];\n  }\n\n  Map<String, dynamic> toJson() => {\"url_image_ani_cut\": urlImageAniCut};\n}\n\nclass Label {}\n\nclass UgcSeason {\n  UgcSeason({\n    this.id,\n    this.title,\n    this.cover,\n    this.mid,\n    this.intro,\n    this.signState,\n    this.attribute,\n    this.sections,\n    this.stat,\n    this.epCount,\n    this.seasonType,\n    this.isPaySeason,\n  });\n\n  int? id;\n  String? title;\n  String? cover;\n  int? mid;\n  String? intro;\n  int? signState;\n  int? attribute;\n  List<SectionItem>? sections;\n  Stat? stat;\n  int? epCount;\n  int? seasonType;\n  bool? isPaySeason;\n\n  UgcSeason.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    title = json['title'];\n    cover = json['cover'];\n    mid = json['mid'];\n    intro = json['intro'];\n    signState = json['sign_state'];\n    attribute = json['attribute'];\n    sections = json['sections'] != null\n        ? json['sections']\n            .map<SectionItem>((e) => SectionItem.fromJson(e))\n            .toList()\n        : [];\n    stat = Stat.fromJson(json['stat']);\n    epCount = json['ep_count'];\n    seasonType = json['season_type'];\n    isPaySeason = json['is_pay_count'];\n  }\n}\n\nclass SectionItem {\n  SectionItem({\n    this.seasonId,\n    this.id,\n    this.title,\n    this.type,\n    this.episodes,\n  });\n\n  int? seasonId;\n  int? id;\n  String? title;\n  int? type;\n  List<EpisodeItem>? episodes;\n\n  SectionItem.fromJson(Map<String, dynamic> json) {\n    seasonId = json['season_id'];\n    id = json['id'];\n    title = json['title'];\n    type = json['type'];\n    episodes = json['episodes']\n        .map<EpisodeItem>((e) => EpisodeItem.fromJson(e))\n        .toList();\n  }\n}\n\nclass EpisodeItem {\n  EpisodeItem({\n    this.seasonId,\n    this.sectionId,\n    this.id,\n    this.aid,\n    this.cid,\n    this.title,\n    this.attribute,\n    this.page,\n    this.bvid,\n    this.cover,\n  });\n  int? seasonId;\n  int? sectionId;\n  int? id;\n  int? aid;\n  int? cid;\n  String? title;\n  int? attribute;\n  Part? page;\n  String? bvid;\n  String? cover;\n\n  EpisodeItem.fromJson(Map<String, dynamic> json) {\n    seasonId = json['season_id'];\n    sectionId = json['sectionId'];\n    id = json['id'];\n    aid = json['aid'];\n    cid = json['cid'];\n    title = json['title'];\n    attribute = json['attribute'];\n    page = Part.fromJson(json['page']);\n    bvid = json['bvid'];\n    cover = json['arc']['pic'];\n  }\n}\n\nclass Staff {\n  Staff({\n    this.mid,\n    this.title,\n    this.name,\n    this.face,\n    this.vip,\n  });\n\n  int? mid;\n  String? title;\n  String? name;\n  String? face;\n  int? status;\n  Vip? vip;\n\n  Staff.fromJson(Map<String, dynamic> json) {\n    mid = json['mid'];\n    title = json['title'];\n    name = json['name'];\n    face = json['face'];\n    vip = Vip.fromJson(json['vip']);\n  }\n}\n\nclass Vip {\n  Vip({\n    this.type,\n    this.status,\n  });\n\n  int? type;\n  int? status;\n\n  Vip.fromJson(Map<String, dynamic> json) {\n    type = json['type'];\n    status = json['status'];\n  }\n}\n"
  },
  {
    "path": "lib/pages/about/index.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\nimport 'package:pilipala/http/index.dart';\nimport 'package:pilipala/models/github/latest.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport '../../utils/cache_manage.dart';\n\nclass AboutPage extends StatefulWidget {\n  const AboutPage({super.key});\n\n  @override\n  State<AboutPage> createState() => _AboutPageState();\n}\n\nclass _AboutPageState extends State<AboutPage> {\n  final AboutController _aboutController = Get.put(AboutController());\n  String cacheSize = '';\n\n  @override\n  void initState() {\n    super.initState();\n    // 读取缓存占用\n    getCacheSize();\n  }\n\n  Future<void> getCacheSize() async {\n    final res = await CacheManage().loadApplicationCache();\n    setState(() => cacheSize = res);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final Color outline = Theme.of(context).colorScheme.outline;\n    TextStyle subTitleStyle =\n        TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);\n    return Scaffold(\n      appBar: AppBar(\n        title: Text('关于', style: Theme.of(context).textTheme.titleMedium),\n      ),\n      body: SingleChildScrollView(\n        child: Column(\n          children: [\n            Image.asset(\n              'assets/images/logo/logo_android_2.png',\n              width: 150,\n            ),\n            Text(\n              'PiliPala',\n              style: Theme.of(context).textTheme.titleMedium,\n            ),\n            const SizedBox(height: 6),\n            Obx(\n              () => Badge(\n                isLabelVisible: _aboutController.isLoading.value\n                    ? false\n                    : _aboutController.isUpdate.value,\n                label: const Text('New'),\n                child: Padding(\n                  padding: const EdgeInsets.fromLTRB(0, 0, 0, 30),\n                  child: FilledButton.tonal(\n                    onPressed: () {\n                      showModalBottomSheet(\n                        context: context,\n                        builder: (context) {\n                          return Column(\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              ListTile(\n                                onTap: () => _aboutController.githubRelease(),\n                                title: const Text('Github下载'),\n                              ),\n                              ListTile(\n                                onTap: () => _aboutController.panDownload(),\n                                title: const Text('网盘下载'),\n                              ),\n                              ListTile(\n                                onTap: () => _aboutController.webSiteUrl(),\n                                title: const Text('官网下载'),\n                              ),\n                              ListTile(\n                                onTap: () => _aboutController.qimiao(),\n                                title: const Text('奇妙应用'),\n                              ),\n                              SizedBox(\n                                  height:\n                                      MediaQuery.of(context).padding.bottom +\n                                          20)\n                            ],\n                          );\n                        },\n                      );\n                    },\n                    child: Text(\n                      'V${_aboutController.currentVersion.value}',\n                      style: subTitleStyle.copyWith(\n                        color: Theme.of(context).primaryColor,\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n            // ListTile(\n            //   onTap: () {},\n            //   title: const Text('更新日志'),\n            //   trailing: const Icon(\n            //     Icons.arrow_forward_ios,\n            //     size: 16,\n            //   ),\n            // ),\n            ListTile(\n              onTap: () => _aboutController.githubUrl(),\n              title: const Text('开源地址'),\n              trailing: Text(\n                'github.com/guozhigq/pilipala',\n                style: subTitleStyle,\n              ),\n            ),\n            ListTile(\n              onTap: () => _aboutController.webSiteUrl(),\n              title: const Text('访问官网'),\n              trailing: Text(\n                'https://pilipalanet.mysxl.cn',\n                style: subTitleStyle,\n              ),\n            ),\n            ListTile(\n              onTap: () => _aboutController.panDownload(),\n              title: const Text('网盘下载'),\n              trailing: Text(\n                '提取码：pili',\n                style: TextStyle(\n                  fontSize: 13,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n              ),\n            ),\n            ListTile(\n              onTap: () => _aboutController.feedback(),\n              title: const Text('问题反馈'),\n              trailing: Icon(\n                Icons.arrow_forward_ios,\n                size: 16,\n                color: outline,\n              ),\n            ),\n            ListTile(\n              onTap: () {\n                showModalBottomSheet(\n                  context: context,\n                  builder: (context) {\n                    return Column(\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        ListTile(\n                          onTap: () => _aboutController.qqChanel(),\n                          title: const Text('QQ群'),\n                          trailing: Text(\n                            '616150809',\n                            style: subTitleStyle,\n                          ),\n                        ),\n                        ListTile(\n                          onTap: () => _aboutController.tgChanel(),\n                          title: const Text('TG频道'),\n                          trailing: Text(\n                            'https://t.me/+lm_oOVmF0RJiODk1',\n                            style: subTitleStyle,\n                          ),\n                        ),\n                        SizedBox(\n                            height: MediaQuery.of(context).padding.bottom + 20)\n                      ],\n                    );\n                  },\n                );\n              },\n              title: const Text('交流社区'),\n              trailing: Icon(\n                Icons.arrow_forward_ios,\n                size: 16,\n                color: outline,\n              ),\n            ),\n            ListTile(\n              onTap: () => _aboutController.aPay(),\n              title: const Text('赞助'),\n              trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),\n            ),\n            ListTile(\n              onTap: () => _aboutController.logs(),\n              title: const Text('错误日志'),\n              trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),\n            ),\n            ListTile(\n              onTap: () async {\n                var cleanStatus = await CacheManage().clearCacheAll();\n                if (cleanStatus) {\n                  getCacheSize();\n                }\n              },\n              title: const Text('清除缓存'),\n              subtitle: Text('图片及网络缓存 $cacheSize', style: subTitleStyle),\n            ),\n            SizedBox(height: MediaQuery.of(context).padding.bottom + 20)\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass AboutController extends GetxController {\n  RxString currentVersion = ''.obs;\n  RxString remoteVersion = ''.obs;\n  late LatestDataModel remoteAppInfo;\n  RxBool isUpdate = false.obs;\n  RxBool isLoading = true.obs;\n  late LatestDataModel data;\n\n  @override\n  void onInit() {\n    super.onInit();\n    // init();\n    // 获取当前版本\n    getCurrentApp();\n    // 获取最新的版本\n    getRemoteApp();\n  }\n\n  // 获取设备信息\n  // Future init() async {\n  //   DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();\n  //   if (Platform.isAndroid) {\n  //     AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;\n  //     print(androidInfo.supportedAbis);\n  //   } else if (Platform.isIOS) {\n  //     IosDeviceInfo iosInfo = await deviceInfo.iosInfo;\n  //     print(iosInfo);\n  //   }\n  // }\n\n  // 获取当前版本\n  Future getCurrentApp() async {\n    var result = await PackageInfo.fromPlatform();\n    currentVersion.value = result.version;\n  }\n\n  // 获取远程版本\n  Future getRemoteApp() async {\n    var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});\n    isLoading.value = false;\n    if (result.data == null || result.data.isEmpty) {\n      SmartDialog.showToast('获取远程版本失败，请检查网络');\n      return;\n    }\n    data = LatestDataModel.fromJson(result.data);\n    remoteAppInfo = data;\n    remoteVersion.value = data.tagName!;\n    isUpdate.value =\n        Utils.needUpdate(currentVersion.value, remoteVersion.value);\n  }\n\n  // 跳转下载/本地更新\n  Future onUpdate() async {\n    Utils.matchVersion(data);\n  }\n\n  // 跳转github\n  githubUrl() {\n    launchUrl(\n      Uri.parse('https://github.com/guozhigq/pilipala'),\n      mode: LaunchMode.externalApplication,\n    );\n  }\n\n  githubRelease() {\n    launchUrl(\n      Uri.parse('https://github.com/guozhigq/pilipala/releases'),\n      mode: LaunchMode.externalApplication,\n    );\n  }\n\n  // 从网盘下载\n  panDownload() {\n    Clipboard.setData(\n      const ClipboardData(text: 'pili'),\n    );\n    SmartDialog.showToast(\n      '已复制提取码：pili',\n      displayTime: const Duration(milliseconds: 500),\n    ).then(\n      (value) => launchUrl(\n        Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'),\n        mode: LaunchMode.externalApplication,\n      ),\n    );\n  }\n\n  // 问题反馈\n  feedback() {\n    launchUrl(\n      Uri.parse('https://github.com/guozhigq/pilipala/issues'),\n      // 系统自带浏览器打开\n      mode: LaunchMode.externalApplication,\n    );\n  }\n\n  // qq频道\n  qqChanel() {\n    Clipboard.setData(\n      const ClipboardData(text: '616150809'),\n    );\n    SmartDialog.showToast('已复制QQ群号');\n  }\n\n  // tg频道\n  tgChanel() {\n    Clipboard.setData(\n      const ClipboardData(text: 'https://t.me/+lm_oOVmF0RJiODk1'),\n    );\n    SmartDialog.showToast(\n      '已复制，即将在浏览器打开',\n      displayTime: const Duration(milliseconds: 500),\n    ).then(\n      (value) => launchUrl(\n        Uri.parse('https://t.me/+lm_oOVmF0RJiODk1'),\n        mode: LaunchMode.externalApplication,\n      ),\n    );\n  }\n\n  aPay() {\n    try {\n      launchUrl(\n        Uri.parse(\n            'alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/fkx14623ddwl1ping3ddd73'),\n        mode: LaunchMode.externalApplication,\n      );\n    } catch (e) {\n      print(e);\n    }\n  }\n\n  // 官网\n  webSiteUrl() {\n    launchUrl(\n      Uri.parse('https://pilipalanet.mysxl.cn'),\n      mode: LaunchMode.externalApplication,\n    );\n  }\n\n  qimiao() {\n    launchUrl(\n      Uri.parse('https://www.magicalapk.com/home'),\n      mode: LaunchMode.externalApplication,\n    );\n  }\n\n  // 日志\n  logs() {\n    Get.toNamed('/logs');\n  }\n}\n"
  },
  {
    "path": "lib/pages/bangumi/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/bangumi.dart';\nimport 'package:pilipala/models/bangumi/list.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass BangumiController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  RxList<BangumiListItemModel> bangumiList = <BangumiListItemModel>[].obs;\n  RxList<BangumiListItemModel> bangumiFollowList = <BangumiListItemModel>[].obs;\n  int _currentPage = 1;\n  bool isLoadingMore = true;\n  Box userInfoCache = GStrorage.userInfo;\n  RxBool userLogin = false.obs;\n  late int mid;\n  var userInfo;\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n    if (userInfo != null) {\n      mid = userInfo.mid;\n    }\n    userLogin.value = userInfo != null;\n  }\n\n  Future queryBangumiListFeed({type = 'init'}) async {\n    if (type == 'init') {\n      _currentPage = 1;\n    }\n    var result = await BangumiHttp.bangumiList(page: _currentPage);\n    if (result['status']) {\n      if (type == 'init') {\n        bangumiList.value = result['data'].list;\n      } else {\n        bangumiList.addAll(result['data'].list);\n      }\n      _currentPage += 1;\n    } else {}\n    isLoadingMore = false;\n    return result;\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    queryBangumiListFeed(type: 'onLoad');\n  }\n\n  // 我的订阅\n  Future queryBangumiFollow() async {\n    userInfo = userInfo ?? userInfoCache.get('userInfoCache');\n    if (userInfo == null) {\n      return;\n    }\n    var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid);\n    if (result['status']) {\n      bangumiFollowList.value = result['data'].list;\n    } else {}\n    return result;\n  }\n\n  // 返回顶部并刷新\n  void animateToTop() async {\n    if (scrollController.offset >=\n        MediaQuery.of(Get.context!).size.height * 5) {\n      scrollController.jumpTo(0);\n    } else {\n      await scrollController.animateTo(0,\n          duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/bangumi/index.dart",
    "content": "library bangumi_panel;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/bangumi/introduction/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/constants.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/bangumi/info.dart';\nimport 'package:pilipala/models/user/fav_folder.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/pages/video/detail/reply/index.dart';\nimport 'package:pilipala/plugin/pl_player/models/play_repeat.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:share_plus/share_plus.dart';\n\nimport '../../../common/pages_bottom_sheet.dart';\nimport '../../../models/common/video_episode_type.dart';\nimport '../../../utils/drawer.dart';\n\nclass BangumiIntroController extends GetxController {\n  // 视频bvid\n  String bvid = Get.parameters['bvid']!;\n  var seasonId = Get.parameters['seasonId'] != null\n      ? int.parse(Get.parameters['seasonId']!)\n      : null;\n  var epId = Get.parameters['epId'] != null\n      ? int.tryParse(Get.parameters['epId']!)\n      : null;\n\n  // 请求状态\n  RxBool isLoading = false.obs;\n\n  // 视频详情 请求返回\n  Rx<BangumiInfoModel> bangumiDetail = BangumiInfoModel().obs;\n\n  // 请求返回的信息\n  String responseMsg = '请求异常';\n\n  // up主粉丝数\n  Map userStat = {'follower': '-'};\n\n  // 是否点赞\n  RxBool hasLike = false.obs;\n  // 是否投币\n  RxBool hasCoin = false.obs;\n  // 是否收藏\n  RxBool hasFav = false.obs;\n  Box userInfoCache = GStrorage.userInfo;\n  bool userLogin = false;\n  Rx<FavFolderData> favFolderData = FavFolderData().obs;\n  List addMediaIdsNew = [];\n  List delMediaIdsNew = [];\n  // 关注状态 默认未关注\n  RxMap followStatus = {}.obs;\n  int _tempThemeValue = -1;\n  var userInfo;\n  PersistentBottomSheetController? bottomSheetController;\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n    userLogin = userInfo != null;\n  }\n\n  // 获取番剧简介&选集\n  Future queryBangumiIntro() async {\n    if (userLogin) {\n      // 获取点赞状态\n      queryHasLikeVideo();\n      // 获取投币状态\n      queryHasCoinVideo();\n      // 获取收藏状态\n      queryHasFavVideo();\n    }\n    var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);\n    if (result['status']) {\n      bangumiDetail.value = result['data'];\n      epId = bangumiDetail.value.episodes!.first.id;\n    }\n    return result;\n  }\n\n  // 获取点赞状态\n  Future queryHasLikeVideo() async {\n    var result = await VideoHttp.hasLikeVideo(bvid: bvid);\n    // data\tnum\t被点赞标志\t0：未点赞  1：已点赞\n    hasLike.value = result[\"data\"] == 1 ? true : false;\n  }\n\n  // 获取投币状态\n  Future queryHasCoinVideo() async {\n    var result = await VideoHttp.hasCoinVideo(bvid: bvid);\n    hasCoin.value = result[\"data\"]['multiply'] == 0 ? false : true;\n  }\n\n  // 获取收藏状态\n  Future queryHasFavVideo() async {\n    var result = await VideoHttp.hasFavVideo(aid: IdUtils.bv2av(bvid));\n    if (result['status']) {\n      hasFav.value = result[\"data\"]['favoured'];\n    } else {\n      hasFav.value = false;\n    }\n  }\n\n  // （取消）点赞\n  Future actionLikeVideo() async {\n    var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);\n    if (result['status']) {\n      SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞');\n      hasLike.value = !hasLike.value;\n      bangumiDetail.value.stat!['likes'] =\n          bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1);\n      hasLike.refresh();\n    } else {\n      SmartDialog.showToast(result['msg']);\n    }\n  }\n\n  // 投币\n  Future actionCoinVideo() async {\n    if (userInfo == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    showDialog(\n        context: Get.context!,\n        builder: (context) {\n          return AlertDialog(\n            title: const Text('选择投币个数'),\n            contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),\n            content: StatefulBuilder(builder: (context, StateSetter setState) {\n              return Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [1, 2]\n                    .map(\n                      (e) => RadioListTile(\n                        value: e,\n                        title: Text('$e枚'),\n                        groupValue: _tempThemeValue,\n                        onChanged: (value) async {\n                          _tempThemeValue = value!;\n                          setState(() {});\n                          var res = await VideoHttp.coinVideo(\n                              bvid: bvid, multiply: _tempThemeValue);\n                          if (res['status']) {\n                            SmartDialog.showToast('投币成功 👏');\n                            hasCoin.value = true;\n                            bangumiDetail.value.stat!['coins'] =\n                                bangumiDetail.value.stat!['coins'] +\n                                    _tempThemeValue;\n                          } else {\n                            SmartDialog.showToast(res['msg']);\n                          }\n                          Get.back();\n                        },\n                      ),\n                    )\n                    .toList(),\n              );\n            }),\n          );\n        });\n  }\n\n  // （取消）收藏\n  Future actionFavVideo() async {\n    try {\n      for (var i in favFolderData.value.list!.toList()) {\n        if (i.favState == 1) {\n          addMediaIdsNew.add(i.id);\n        } else {\n          delMediaIdsNew.add(i.id);\n        }\n      }\n    } catch (_) {}\n    var result = await VideoHttp.favVideo(\n        aid: IdUtils.bv2av(bvid),\n        addIds: addMediaIdsNew.join(','),\n        delIds: delMediaIdsNew.join(','));\n    if (result['status']) {\n      addMediaIdsNew = [];\n      delMediaIdsNew = [];\n      // 重新获取收藏状态\n      queryHasFavVideo();\n      SmartDialog.showToast('✅ 操作成功');\n      Get.back();\n    }\n  }\n\n  // 分享视频\n  Future actionShareVideo() async {\n    var result = await Share.share('${HttpString.baseUrl}/video/$bvid')\n        .whenComplete(() {});\n    return result;\n  }\n\n  // 选择文件夹\n  onChoose(bool checkValue, int index) {\n    feedBack();\n    List<FavFolderItemData> datalist = favFolderData.value.list!;\n    for (var i = 0; i < datalist.length; i++) {\n      if (i == index) {\n        datalist[i].favState = checkValue == true ? 1 : 0;\n        datalist[i].mediaCount = checkValue == true\n            ? datalist[i].mediaCount! + 1\n            : datalist[i].mediaCount! - 1;\n      }\n    }\n    favFolderData.value.list = datalist;\n    favFolderData.refresh();\n  }\n\n  // 修改分P或番剧分集\n  Future changeSeasonOrbangu(bvid, cid, aid, cover) async {\n    // 重新获取视频资源\n    VideoDetailController videoDetailCtr =\n        Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);\n    videoDetailCtr.bvid = bvid;\n    videoDetailCtr.cid.value = cid;\n    videoDetailCtr.danmakuCid.value = cid;\n    videoDetailCtr.oid.value = aid;\n    videoDetailCtr.cover.value = cover;\n    videoDetailCtr.queryVideoUrl();\n    videoDetailCtr.getSubtitle();\n    videoDetailCtr.setSubtitleContent();\n    // 重新请求评论\n    try {\n      /// 未渲染回复组件时可能异常\n      VideoReplyController videoReplyCtr =\n          Get.find<VideoReplyController>(tag: Get.arguments['heroTag']);\n      videoReplyCtr.aid = aid;\n      videoReplyCtr.queryReplyList(type: 'init');\n    } catch (_) {}\n  }\n\n  // 追番\n  Future bangumiAdd() async {\n    var result =\n        await VideoHttp.bangumiAdd(seasonId: bangumiDetail.value.seasonId);\n    SmartDialog.showToast(result['msg']);\n  }\n\n  // 取消追番\n  Future bangumiDel() async {\n    var result =\n        await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId);\n    SmartDialog.showToast(result['msg']);\n  }\n\n  Future queryVideoInFolder() async {\n    var result = await VideoHttp.videoInFolder(\n        mid: userInfo.mid, rid: IdUtils.bv2av(bvid));\n    if (result['status']) {\n      favFolderData.value = result['data'];\n    }\n    return result;\n  }\n\n  /// 列表循环或者顺序播放时，自动播放下一个\n  void nextPlay() {\n    late List episodes;\n    if (bangumiDetail.value.episodes != null) {\n      episodes = bangumiDetail.value.episodes!;\n    }\n    VideoDetailController videoDetailCtr =\n        Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);\n    int currentIndex =\n        episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);\n    int nextIndex = currentIndex + 1;\n    PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;\n    // 列表循环\n    if (platRepeat == PlayRepeat.listCycle) {\n      if (nextIndex == episodes.length - 1) {\n        nextIndex = 0;\n      }\n    }\n    if (nextIndex <= episodes.length - 1 &&\n        platRepeat == PlayRepeat.listOrder) {}\n\n    int cid = episodes[nextIndex].cid!;\n    String bvid = episodes[nextIndex].bvid!;\n    int aid = episodes[nextIndex].aid!;\n    String cover = episodes[nextIndex].cover!;\n    changeSeasonOrbangu(bvid, cid, aid, cover);\n  }\n\n  // 播放器底栏 选集 回调\n  void showEposideHandler() {\n    late List episodes = bangumiDetail.value.episodes!;\n    VideoEpidoesType dataType = VideoEpidoesType.bangumiEpisode;\n    if (episodes.isEmpty) {\n      return;\n    }\n    VideoDetailController videoDetailCtr =\n        Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);\n    DrawerUtils.showRightDialog(\n      child: EpisodeBottomSheet(\n        episodes: episodes,\n        currentCid: videoDetailCtr.cid.value,\n        dataType: dataType,\n        context: Get.context!,\n        sheetHeight: Get.size.height,\n        isFullScreen: true,\n        changeFucCall: (item, index) {\n          changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover);\n          SmartDialog.dismiss();\n        },\n      ).buildShowContent(Get.context!),\n    );\n  }\n\n  hiddenEpisodeBottomSheet() {\n    bottomSheetController?.close();\n  }\n}\n"
  },
  {
    "path": "lib/pages/bangumi/introduction/index.dart",
    "content": "library bangumi_intro;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/bangumi/introduction/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/stat/danmu.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/models/bangumi/info.dart';\nimport 'package:pilipala/pages/bangumi/widgets/bangumi_panel.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/pages/video/detail/introduction/widgets/action_item.dart';\nimport 'package:pilipala/pages/video/detail/introduction/widgets/fav_panel.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport '../../../common/widgets/http_error.dart';\nimport 'controller.dart';\nimport 'widgets/intro_detail.dart';\n\nclass BangumiIntroPanel extends StatefulWidget {\n  final int? cid;\n  const BangumiIntroPanel({\n    Key? key,\n    this.cid,\n  }) : super(key: key);\n\n  @override\n  State<BangumiIntroPanel> createState() => _BangumiIntroPanelState();\n}\n\nclass _BangumiIntroPanelState extends State<BangumiIntroPanel>\n    with AutomaticKeepAliveClientMixin {\n  late BangumiIntroController bangumiIntroController;\n  late VideoDetailController videoDetailCtr;\n  BangumiInfoModel? bangumiDetail;\n  late Future _futureBuilderFuture;\n  late int cid;\n  late String heroTag;\n\n// 添加页面缓存\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    heroTag = Get.arguments['heroTag'];\n    cid = widget.cid!;\n    bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);\n    videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);\n    _futureBuilderFuture = bangumiIntroController.queryBangumiIntro();\n    videoDetailCtr.cid.listen((int p0) {\n      cid = p0;\n      setState(() {});\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return FutureBuilder(\n      future: _futureBuilderFuture,\n      builder: (BuildContext context, AsyncSnapshot snapshot) {\n        if (snapshot.connectionState == ConnectionState.done) {\n          if (snapshot.data == null) {\n            return const SliverToBoxAdapter(child: SizedBox());\n          }\n          if (snapshot.data['status']) {\n            // 请求成功\n            return Obx(\n              () => BangumiInfo(\n                bangumiDetail: bangumiIntroController.bangumiDetail.value,\n                cid: cid,\n              ),\n            );\n          } else {\n            // 请求错误\n            return HttpError(\n              errMsg: snapshot.data['msg'],\n              fn: () => Get.back(),\n            );\n          }\n        } else {\n          return const SliverToBoxAdapter(\n            child: SizedBox(\n              height: 100,\n              child: Center(\n                child: CircularProgressIndicator(),\n              ),\n            ),\n          );\n        }\n      },\n    );\n  }\n}\n\nclass BangumiInfo extends StatefulWidget {\n  const BangumiInfo({\n    super.key,\n    this.bangumiDetail,\n    this.cid,\n  });\n\n  final BangumiInfoModel? bangumiDetail;\n  final int? cid;\n\n  @override\n  State<BangumiInfo> createState() => _BangumiInfoState();\n}\n\nclass _BangumiInfoState extends State<BangumiInfo> {\n  String heroTag = Get.arguments['heroTag'];\n  late final BangumiIntroController bangumiIntroController;\n  late final VideoDetailController videoDetailCtr;\n  Box localCache = GStrorage.localCache;\n  late double sheetHeight;\n  int? cid;\n  bool isProcessing = false;\n  void Function()? handleState(Future Function() action) {\n    return isProcessing\n        ? null\n        : () async {\n            setState(() => isProcessing = true);\n            await action();\n            setState(() => isProcessing = false);\n          };\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);\n    videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);\n    sheetHeight = localCache.get('sheetHeight');\n    cid = widget.cid!;\n    videoDetailCtr.cid.listen((p0) {\n      cid = p0;\n      if (!mounted) {\n        return;\n      }\n      setState(() {});\n    });\n  }\n\n  // 收藏\n  showFavBottomSheet() {\n    if (bangumiIntroController.userInfo.mid == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    showModalBottomSheet(\n      context: context,\n      useRootNavigator: true,\n      isScrollControlled: true,\n      builder: (BuildContext context) {\n        return FavPanel(ctr: bangumiIntroController);\n      },\n    );\n  }\n\n  // 视频介绍\n  showIntroDetail() {\n    feedBack();\n    showBottomSheet(\n      context: context,\n      enableDrag: true,\n      builder: (BuildContext context) {\n        return IntroDetail(bangumiDetail: widget.bangumiDetail!);\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final ThemeData t = Theme.of(context);\n    return SliverPadding(\n      padding: const EdgeInsets.only(\n          left: StyleString.safeSpace, right: StyleString.safeSpace, top: 20),\n      sliver: SliverToBoxAdapter(\n          child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Row(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Stack(\n                children: [\n                  NetworkImgLayer(\n                    width: 115,\n                    height: 115 / 0.75,\n                    src: widget.bangumiDetail!.cover!,\n                  ),\n                  PBadge(\n                    text:\n                        '评分 ${widget.bangumiDetail?.rating?['score']! ?? '暂无'}',\n                    top: null,\n                    right: 6,\n                    bottom: 6,\n                    left: null,\n                  ),\n                ],\n              ),\n              const SizedBox(width: 10),\n              Expanded(\n                child: InkWell(\n                  onTap: () => showIntroDetail(),\n                  child: SizedBox(\n                    height: 115 / 0.75,\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        Row(\n                          children: [\n                            Expanded(\n                              child: Text(\n                                widget.bangumiDetail!.title!,\n                                style: const TextStyle(\n                                  fontSize: 16,\n                                  fontWeight: FontWeight.w500,\n                                ),\n                                maxLines: 1,\n                                overflow: TextOverflow.ellipsis,\n                              ),\n                            ),\n                            const SizedBox(width: 20),\n                            SizedBox(\n                              width: 34,\n                              height: 34,\n                              child: IconButton(\n                                style: ButtonStyle(\n                                  padding: MaterialStateProperty.all(\n                                      EdgeInsets.zero),\n                                  backgroundColor:\n                                      MaterialStateProperty.resolveWith(\n                                          (Set<MaterialState> states) {\n                                    return t.colorScheme.primaryContainer\n                                        .withOpacity(0.7);\n                                  }),\n                                ),\n                                onPressed: () =>\n                                    bangumiIntroController.bangumiAdd(),\n                                icon: Icon(\n                                  Icons.favorite_border_rounded,\n                                  color: t.colorScheme.primary,\n                                  size: 22,\n                                ),\n                              ),\n                            ),\n                          ],\n                        ),\n                        Row(\n                          children: [\n                            StatView(\n                              view: widget.bangumiDetail!.stat!['views'],\n                              size: 'medium',\n                            ),\n                            const SizedBox(width: 6),\n                            StatDanMu(\n                              danmu: widget.bangumiDetail!.stat!['danmakus'],\n                              size: 'medium',\n                            ),\n                          ],\n                        ),\n                        const SizedBox(height: 6),\n                        Row(\n                          children: [\n                            Text(\n                              (widget.bangumiDetail!.areas!.isNotEmpty\n                                  ? widget.bangumiDetail!.areas!.first['name']\n                                  : ''),\n                              style: TextStyle(\n                                fontSize: 12,\n                                color: t.colorScheme.outline,\n                              ),\n                            ),\n                            const SizedBox(width: 6),\n                            Text(\n                              widget.bangumiDetail!.publish!['pub_time_show'],\n                              style: TextStyle(\n                                fontSize: 12,\n                                color: t.colorScheme.outline,\n                              ),\n                            ),\n                          ],\n                        ),\n                        Text(\n                          widget.bangumiDetail!.newEp!['desc'],\n                          style: TextStyle(\n                            fontSize: 12,\n                            color: t.colorScheme.outline,\n                          ),\n                        ),\n                        const Spacer(),\n                        Text(\n                          '简介：${widget.bangumiDetail!.evaluate!}',\n                          maxLines: 3,\n                          overflow: TextOverflow.ellipsis,\n                          style: TextStyle(\n                            fontSize: 13,\n                            color: t.colorScheme.outline,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ),\n          const SizedBox(height: 6),\n\n          /// 点赞收藏转发\n          actionGrid(context, bangumiIntroController),\n          // 番剧分p\n          if (widget.bangumiDetail!.episodes!.isNotEmpty) ...[\n            BangumiPanel(\n              pages: widget.bangumiDetail!.episodes!,\n              cid: cid! ?? widget.bangumiDetail!.episodes!.first.cid!,\n              sheetHeight: sheetHeight,\n              changeFuc: (bvid, cid, aid, cover) => bangumiIntroController\n                  .changeSeasonOrbangu(bvid, cid, aid, cover),\n              bangumiDetail: bangumiIntroController.bangumiDetail.value,\n              bangumiIntroController: bangumiIntroController,\n            )\n          ],\n        ],\n      )),\n    );\n  }\n\n  Widget actionGrid(BuildContext context, bangumiIntroController) {\n    return LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n      return Material(\n        child: Padding(\n          padding: const EdgeInsets.only(top: 16, bottom: 8),\n          child: SizedBox(\n            height: constraints.maxWidth / 5 * 0.8,\n            child: GridView.count(\n              primary: false,\n              padding: EdgeInsets.zero,\n              crossAxisCount: 5,\n              childAspectRatio: 1.25,\n              children: <Widget>[\n                Obx(\n                  () => ActionItem(\n                    icon: const Icon(FontAwesomeIcons.thumbsUp),\n                    selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),\n                    onTap: handleState(bangumiIntroController.actionLikeVideo),\n                    selectStatus: bangumiIntroController.hasLike.value,\n                    text: widget.bangumiDetail!.stat!['likes']!.toString(),\n                  ),\n                ),\n                Obx(\n                  () => ActionItem(\n                    icon: const Icon(FontAwesomeIcons.b),\n                    selectIcon: const Icon(FontAwesomeIcons.b),\n                    onTap: handleState(bangumiIntroController.actionCoinVideo),\n                    selectStatus: bangumiIntroController.hasCoin.value,\n                    text: widget.bangumiDetail!.stat!['coins']!.toString(),\n                  ),\n                ),\n                Obx(\n                  () => ActionItem(\n                    icon: const Icon(FontAwesomeIcons.star),\n                    selectIcon: const Icon(FontAwesomeIcons.solidStar),\n                    onTap: () => showFavBottomSheet(),\n                    selectStatus: bangumiIntroController.hasFav.value,\n                    text: widget.bangumiDetail!.stat!['favorite']!.toString(),\n                  ),\n                ),\n                ActionItem(\n                  icon: const Icon(FontAwesomeIcons.comment),\n                  selectIcon: const Icon(FontAwesomeIcons.reply),\n                  onTap: () => videoDetailCtr.tabCtr.animateTo(1),\n                  selectStatus: false,\n                  text: widget.bangumiDetail!.stat!['reply']!.toString(),\n                ),\n                ActionItem(\n                  icon: const Icon(FontAwesomeIcons.shareFromSquare),\n                  onTap: () => bangumiIntroController.actionShareVideo(),\n                  selectStatus: false,\n                  text: widget.bangumiDetail!.stat!['share']!.toString(),\n                ),\n              ],\n            ),\n          ),\n        ),\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/bangumi/introduction/widgets/intro_detail.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/widgets/stat/danmu.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nBox localCache = GStrorage.localCache;\nlate double sheetHeight;\n\nclass IntroDetail extends StatelessWidget {\n  final dynamic bangumiDetail;\n\n  const IntroDetail({\n    Key? key,\n    this.bangumiDetail,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    sheetHeight = localCache.get('sheetHeight');\n    TextStyle smallTitle = TextStyle(\n      fontSize: 12,\n      color: Theme.of(context).colorScheme.onSurface,\n    );\n    return Container(\n      color: Theme.of(context).colorScheme.surface,\n      padding: const EdgeInsets.only(left: 14, right: 14),\n      height: sheetHeight,\n      child: Column(\n        children: [\n          Container(\n            height: 35,\n            padding: const EdgeInsets.only(bottom: 2),\n            child: Center(\n              child: Container(\n                width: 32,\n                height: 3,\n                decoration: BoxDecoration(\n                    color: Theme.of(context)\n                        .colorScheme\n                        .onSecondaryContainer\n                        .withOpacity(0.5),\n                    borderRadius: const BorderRadius.all(Radius.circular(3))),\n              ),\n            ),\n          ),\n          Expanded(\n            child: SingleChildScrollView(\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Text(\n                    bangumiDetail!.title,\n                    style: const TextStyle(\n                      fontSize: 16,\n                      fontWeight: FontWeight.w500,\n                    ),\n                  ),\n                  const SizedBox(height: 4),\n                  Row(\n                    children: [\n                      StatView(\n                        view: bangumiDetail!.stat!['views'],\n                        size: 'medium',\n                      ),\n                      const SizedBox(width: 6),\n                      StatDanMu(\n                        danmu: bangumiDetail!.stat!['danmakus'],\n                        size: 'medium',\n                      ),\n                    ],\n                  ),\n                  const SizedBox(height: 4),\n                  Row(\n                    children: [\n                      Text(\n                        bangumiDetail!.areas!.first['name'],\n                        style: smallTitle,\n                      ),\n                      const SizedBox(width: 6),\n                      Text(\n                        bangumiDetail!.publish!['pub_time_show'],\n                        style: smallTitle,\n                      ),\n                      const SizedBox(width: 6),\n                      Text(\n                        bangumiDetail!.newEp!['desc'],\n                        style: smallTitle,\n                      ),\n                    ],\n                  ),\n                  const SizedBox(height: 20),\n                  Text(\n                    '简介：',\n                    style: Theme.of(context).textTheme.titleMedium,\n                  ),\n                  const SizedBox(height: 4),\n                  Text(\n                    '${bangumiDetail!.evaluate!}',\n                    style: smallTitle.copyWith(fontSize: 13),\n                  ),\n                  const SizedBox(height: 20),\n                  Text(\n                    '声优：',\n                    style: Theme.of(context).textTheme.titleMedium,\n                  ),\n                  const SizedBox(height: 4),\n                  Text(\n                    bangumiDetail.actors,\n                    style: smallTitle.copyWith(fontSize: 13),\n                  ),\n                  SizedBox(height: MediaQuery.of(context).padding.bottom + 20)\n                ],\n              ),\n            ),\n          )\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/bangumi/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/utils/main_stream.dart';\n\nimport 'controller.dart';\nimport 'widgets/bangumu_card_v.dart';\n\nclass BangumiPage extends StatefulWidget {\n  const BangumiPage({super.key});\n\n  @override\n  State<BangumiPage> createState() => _BangumiPageState();\n}\n\nclass _BangumiPageState extends State<BangumiPage>\n    with AutomaticKeepAliveClientMixin {\n  final BangumiController _bangumidController = Get.put(BangumiController());\n  late Future? _futureBuilderFuture;\n  late Future? _futureBuilderFutureFollow;\n  late ScrollController scrollController;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    scrollController = _bangumidController.scrollController;\n    _futureBuilderFuture = _bangumidController.queryBangumiListFeed();\n    _futureBuilderFutureFollow = _bangumidController.queryBangumiFollow();\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {\n            _bangumidController.isLoadingMore = true;\n            _bangumidController.onLoad();\n          });\n        }\n        handleScrollEvent(scrollController);\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return RefreshIndicator(\n      onRefresh: () async {\n        await _bangumidController.queryBangumiListFeed();\n        return _bangumidController.queryBangumiFollow();\n      },\n      child: CustomScrollView(\n        controller: _bangumidController.scrollController,\n        slivers: [\n          SliverToBoxAdapter(\n            child: Obx(\n              () => Visibility(\n                visible: _bangumidController.userLogin.value,\n                child: Column(\n                  children: [\n                    Padding(\n                      padding: const EdgeInsets.only(\n                          top: StyleString.safeSpace, bottom: 10, left: 16),\n                      child: Row(\n                        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                        children: [\n                          Text(\n                            '最近追番',\n                            style: Theme.of(context).textTheme.titleMedium,\n                          ),\n                          IconButton(\n                            onPressed: () {\n                              setState(() {\n                                _futureBuilderFutureFollow =\n                                    _bangumidController.queryBangumiFollow();\n                              });\n                            },\n                            icon: const Icon(\n                              Icons.refresh,\n                              size: 20,\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                    SizedBox(\n                      height: Get.size.width / 3 / 0.75 +\n                          MediaQuery.textScalerOf(context).scale(50.0),\n                      child: FutureBuilder(\n                        future: _futureBuilderFutureFollow,\n                        builder:\n                            (BuildContext context, AsyncSnapshot snapshot) {\n                          if (snapshot.connectionState ==\n                              ConnectionState.done) {\n                            if (snapshot.data == null) {\n                              return const SizedBox();\n                            }\n                            Map data = snapshot.data as Map;\n                            List list = _bangumidController.bangumiFollowList;\n                            if (data['status']) {\n                              return Obx(\n                                () => list.isNotEmpty\n                                    ? ListView.builder(\n                                        scrollDirection: Axis.horizontal,\n                                        itemCount: list.length,\n                                        itemBuilder: (context, index) {\n                                          return Container(\n                                            width: Get.size.width / 3,\n                                            margin: EdgeInsets.only(\n                                                left: StyleString.safeSpace,\n                                                right: index ==\n                                                        _bangumidController\n                                                                .bangumiFollowList\n                                                                .length -\n                                                            1\n                                                    ? StyleString.safeSpace\n                                                    : 0),\n                                            child: BangumiCardV(\n                                              bangumiItem: _bangumidController\n                                                  .bangumiFollowList[index],\n                                            ),\n                                          );\n                                        },\n                                      )\n                                    : const SizedBox(\n                                        child: Center(\n                                          child: Text('还没有追番'),\n                                        ),\n                                      ),\n                              );\n                            } else {\n                              return const SizedBox();\n                            }\n                          } else {\n                            return const SizedBox();\n                          }\n                        },\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n          SliverToBoxAdapter(\n            child: Padding(\n              padding: const EdgeInsets.only(top: 10, bottom: 10, left: 16),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                children: [\n                  Text(\n                    '推荐',\n                    style: Theme.of(context).textTheme.titleMedium,\n                  ),\n                ],\n              ),\n            ),\n          ),\n          SliverPadding(\n            padding: const EdgeInsets.fromLTRB(\n                StyleString.safeSpace, 0, StyleString.safeSpace, 0),\n            sliver: FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (BuildContext context, AsyncSnapshot snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  Map data = snapshot.data as Map;\n                  if (data['status']) {\n                    return Obx(() => contentGrid(\n                        _bangumidController, _bangumidController.bangumiList));\n                  } else {\n                    return HttpError(\n                      errMsg: data['msg'],\n                      fn: () {\n                        _futureBuilderFuture =\n                            _bangumidController.queryBangumiListFeed();\n                      },\n                    );\n                  }\n                } else {\n                  return contentGrid(_bangumidController, []);\n                }\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget contentGrid(ctr, bangumiList) {\n    return SliverGrid(\n      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n        // 行间距\n        mainAxisSpacing: StyleString.cardSpace - 2,\n        // 列间距\n        crossAxisSpacing: StyleString.cardSpace,\n        // 列数\n        crossAxisCount: 3,\n        mainAxisExtent: Get.size.width / 3 / 0.75 +\n            MediaQuery.textScalerOf(context).scale(42.0),\n      ),\n      delegate: SliverChildBuilderDelegate(\n        (BuildContext context, int index) {\n          return bangumiList!.isNotEmpty\n              ? BangumiCardV(bangumiItem: bangumiList[index])\n              : const SizedBox();\n        },\n        childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/bangumi/widgets/bangumi_panel.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/bangumi/info.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:scrollable_positioned_list/scrollable_positioned_list.dart';\nimport '../../../common/pages_bottom_sheet.dart';\nimport '../../../models/common/video_episode_type.dart';\nimport '../introduction/controller.dart';\n\nclass BangumiPanel extends StatefulWidget {\n  const BangumiPanel({\n    super.key,\n    required this.pages,\n    required this.cid,\n    this.sheetHeight,\n    this.changeFuc,\n    this.bangumiDetail,\n    this.bangumiIntroController,\n  });\n\n  final List<EpisodeItem> pages;\n  final int cid;\n  final double? sheetHeight;\n  final Function? changeFuc;\n  final BangumiInfoModel? bangumiDetail;\n  final BangumiIntroController? bangumiIntroController;\n\n  @override\n  State<BangumiPanel> createState() => _BangumiPanelState();\n}\n\nclass _BangumiPanelState extends State<BangumiPanel> {\n  late RxInt currentIndex = (-1).obs;\n  final ScrollController listViewScrollCtr = ScrollController();\n  Box userInfoCache = GStrorage.userInfo;\n  dynamic userInfo;\n  // 默认未开通\n  int vipStatus = 0;\n  late int cid;\n  String heroTag = Get.arguments['heroTag'];\n  late final VideoDetailController videoDetailCtr;\n  final ItemScrollController itemScrollController = ItemScrollController();\n  late PersistentBottomSheetController? _bottomSheetController;\n\n  @override\n  void initState() {\n    super.initState();\n    cid = widget.cid;\n    videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);\n    currentIndex.value =\n        widget.pages.indexWhere((EpisodeItem e) => e.cid == cid);\n    scrollToIndex();\n    videoDetailCtr.cid.listen((int p0) {\n      cid = p0;\n      currentIndex.value =\n          widget.pages.indexWhere((EpisodeItem e) => e.cid == cid);\n      scrollToIndex();\n    });\n\n    /// 获取大会员状态\n    userInfo = userInfoCache.get('userInfoCache');\n    if (userInfo != null) {\n      vipStatus = userInfo.vipStatus;\n    }\n  }\n\n  @override\n  void dispose() {\n    listViewScrollCtr.dispose();\n    super.dispose();\n  }\n\n  void changeFucCall(item, i) async {\n    if (item.badge != null && item.badge == '会员' && vipStatus != 1) {\n      SmartDialog.showToast('需要大会员');\n      return;\n    }\n    widget.changeFuc?.call(\n      item.bvid,\n      item.cid,\n      item.aid,\n      item.cover,\n    );\n    try {\n      if (_bottomSheetController != null) {\n        _bottomSheetController?.close();\n      }\n    } catch (_) {}\n    currentIndex.value = i;\n    scrollToIndex();\n  }\n\n  void scrollToIndex() {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      // 在回调函数中获取更新后的状态\n      final double offset = min((currentIndex * 150) - 75,\n          listViewScrollCtr.position.maxScrollExtent);\n      if (currentIndex.value == 0) {\n        listViewScrollCtr.jumpTo(0);\n      } else {\n        listViewScrollCtr.animateTo(\n          offset,\n          duration: const Duration(milliseconds: 300),\n          curve: Curves.easeInOut,\n        );\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Color primary = Theme.of(context).colorScheme.primary;\n    Color onSurface = Theme.of(context).colorScheme.onSurface;\n    return Column(\n      children: [\n        Padding(\n          padding: const EdgeInsets.only(top: 10, bottom: 10),\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            children: [\n              const Text('选集 '),\n              Expanded(\n                child: Obx(\n                  () => Text(\n                    ' 正在播放：${widget.pages[currentIndex.value].longTitle}',\n                    overflow: TextOverflow.ellipsis,\n                    style: TextStyle(\n                      fontSize: 12,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                  ),\n                ),\n              ),\n              const SizedBox(width: 10),\n              SizedBox(\n                height: 34,\n                child: TextButton(\n                  style: ButtonStyle(\n                    padding: MaterialStateProperty.all(EdgeInsets.zero),\n                  ),\n                  onPressed: () {\n                    widget.bangumiIntroController?.bottomSheetController =\n                        _bottomSheetController = EpisodeBottomSheet(\n                      currentCid: cid,\n                      episodes: widget.pages,\n                      changeFucCall: changeFucCall,\n                      sheetHeight: widget.sheetHeight,\n                      dataType: VideoEpidoesType.bangumiEpisode,\n                      context: context,\n                    ).show(context);\n                  },\n                  child: Text(\n                    '${widget.bangumiDetail!.newEp!['desc']}',\n                    style: const TextStyle(fontSize: 13),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n        SizedBox(\n          height: 60,\n          child: ListView.builder(\n            controller: listViewScrollCtr,\n            scrollDirection: Axis.horizontal,\n            itemCount: widget.pages.length,\n            itemExtent: 150,\n            itemBuilder: (BuildContext context, int i) {\n              var page = widget.pages[i];\n              bool isSelected = i == currentIndex.value;\n              return Container(\n                width: 150,\n                margin: const EdgeInsets.only(right: 10),\n                child: Material(\n                  color: Theme.of(context).colorScheme.onInverseSurface,\n                  borderRadius: BorderRadius.circular(6),\n                  clipBehavior: Clip.hardEdge,\n                  child: InkWell(\n                    onTap: () => changeFucCall(page, i),\n                    child: Padding(\n                      padding: const EdgeInsets.symmetric(\n                        vertical: 8,\n                        horizontal: 10,\n                      ),\n                      child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: <Widget>[\n                          Row(\n                            children: [\n                              if (isSelected) ...<Widget>[\n                                Image.asset('assets/images/live.png',\n                                    color: primary, height: 12),\n                                const SizedBox(width: 6)\n                              ],\n                              Text(\n                                '第${i + 1}话',\n                                style: TextStyle(\n                                  fontSize: 13,\n                                  color: isSelected ? primary : onSurface,\n                                ),\n                              ),\n                              const SizedBox(width: 2),\n                              if (page.badge != null) ...[\n                                const Spacer(),\n                                Text(\n                                  page.badge!,\n                                  style: TextStyle(\n                                    fontSize: 12,\n                                    color: primary,\n                                  ),\n                                ),\n                              ]\n                            ],\n                          ),\n                          const SizedBox(height: 3),\n                          Text(\n                            page.longTitle!,\n                            maxLines: 1,\n                            style: TextStyle(\n                              fontSize: 13,\n                              color: isSelected ? primary : onSurface,\n                            ),\n                            overflow: TextOverflow.ellipsis,\n                          )\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        )\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/bangumi/widgets/bangumu_card_v.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/models/bangumi/list.dart';\nimport 'package:pilipala/utils/image_save.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\n\n// 视频卡片 - 垂直布局\nclass BangumiCardV extends StatelessWidget {\n  const BangumiCardV({\n    super.key,\n    required this.bangumiItem,\n  });\n\n  final BangumiListItemModel bangumiItem;\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(bangumiItem.mediaId);\n    return InkWell(\n      onTap: () {\n        RoutePush.bangumiPush(\n          bangumiItem.seasonId,\n          null,\n          heroTag: heroTag,\n        );\n      },\n      onLongPress: () =>\n          imageSaveDialog(context, bangumiItem, SmartDialog.dismiss),\n      child: Column(\n        children: [\n          ClipRRect(\n            borderRadius: const BorderRadius.all(\n              StyleString.imgRadius,\n            ),\n            child: AspectRatio(\n              aspectRatio: 0.75,\n              child: LayoutBuilder(builder: (context, boxConstraints) {\n                final double maxWidth = boxConstraints.maxWidth;\n                final double maxHeight = boxConstraints.maxHeight;\n                return Stack(\n                  children: [\n                    Hero(\n                      tag: heroTag,\n                      child: NetworkImgLayer(\n                        src: bangumiItem.cover,\n                        width: maxWidth,\n                        height: maxHeight,\n                      ),\n                    ),\n                    if (bangumiItem.badge != null)\n                      PBadge(\n                          text: bangumiItem.badge,\n                          top: 6,\n                          right: 6,\n                          bottom: null,\n                          left: null),\n                    if (bangumiItem.order != null)\n                      PBadge(\n                        text: bangumiItem.order,\n                        top: null,\n                        right: null,\n                        bottom: 6,\n                        left: 6,\n                        type: 'gray',\n                      ),\n                  ],\n                );\n              }),\n            ),\n          ),\n          BangumiContent(bangumiItem: bangumiItem)\n        ],\n      ),\n    );\n  }\n}\n\nclass BangumiContent extends StatelessWidget {\n  const BangumiContent({super.key, required this.bangumiItem});\n  // ignore: prefer_typing_uninitialized_variables\n  final bangumiItem;\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: Padding(\n        // 多列\n        padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),\n        // 单列\n        // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          // mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            Row(\n              children: [\n                Expanded(\n                    child: Text(\n                  bangumiItem.title,\n                  textAlign: TextAlign.start,\n                  style: const TextStyle(\n                    fontWeight: FontWeight.w500,\n                    letterSpacing: 0.3,\n                  ),\n                  maxLines: 1,\n                  overflow: TextOverflow.ellipsis,\n                )),\n              ],\n            ),\n            const SizedBox(height: 1),\n            if (bangumiItem.indexShow != null)\n              Text(\n                bangumiItem.indexShow,\n                maxLines: 1,\n                style: TextStyle(\n                  fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n              ),\n            if (bangumiItem.progress != null)\n              Text(\n                bangumiItem.progress,\n                maxLines: 1,\n                style: TextStyle(\n                  fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/blacklist/index.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/black.dart';\nimport 'package:pilipala/models/user/black.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass BlackListPage extends StatefulWidget {\n  const BlackListPage({super.key});\n\n  @override\n  State<BlackListPage> createState() => _BlackListPageState();\n}\n\nclass _BlackListPageState extends State<BlackListPage> {\n  final BlackListController _blackListController =\n      Get.put(BlackListController());\n  final ScrollController scrollController = ScrollController();\n  Future? _futureBuilderFuture;\n  bool _isLoadingMore = false;\n  Box setting = GStrorage.setting;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _blackListController.queryBlacklist();\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          if (!_isLoadingMore) {\n            _isLoadingMore = true;\n            await _blackListController.queryBlacklist(type: 'onLoad');\n            _isLoadingMore = false;\n          }\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    List<int> blackMidsList =\n        _blackListController.blackList.map<int>((e) => e.mid!).toList();\n    setting.put(SettingBoxKey.blackMidsList, blackMidsList);\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        elevation: 0,\n        scrolledUnderElevation: 0,\n        titleSpacing: 0,\n        centerTitle: false,\n        title: Obx(\n          () => Text(\n            '黑名单管理 ${_blackListController.total.value == 0 ? '' : '- ${_blackListController.total.value}'}',\n            style: Theme.of(context).textTheme.titleMedium,\n          ),\n        ),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async => await _blackListController.queryBlacklist(),\n        child: FutureBuilder(\n          future: _futureBuilderFuture,\n          builder: (BuildContext context, AsyncSnapshot snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              var data = snapshot.data;\n              if (data['status']) {\n                List<BlackListItem> list = _blackListController.blackList;\n                return Obx(\n                  () => list.isEmpty\n                      ? CustomScrollView(\n                          slivers: [\n                            HttpError(errMsg: '你没有拉黑任何人哦～_～', fn: () => {})\n                          ],\n                        )\n                      : ListView.builder(\n                          controller: scrollController,\n                          itemCount: list.length,\n                          itemBuilder: (BuildContext context, int index) {\n                            return ListTile(\n                              onTap: () {},\n                              leading: NetworkImgLayer(\n                                width: 45,\n                                height: 45,\n                                type: 'avatar',\n                                src: list[index].face,\n                              ),\n                              title: Text(\n                                list[index].uname!,\n                                maxLines: 1,\n                                overflow: TextOverflow.ellipsis,\n                                style: const TextStyle(fontSize: 14),\n                              ),\n                              subtitle: Text(\n                                Utils.dateFormat(list[index].mtime),\n                                maxLines: 1,\n                                style: TextStyle(\n                                    color:\n                                        Theme.of(context).colorScheme.outline),\n                                overflow: TextOverflow.ellipsis,\n                              ),\n                              dense: true,\n                              trailing: TextButton(\n                                onPressed: () => _blackListController\n                                    .removeBlack(list[index].mid),\n                                child: const Text('移除'),\n                              ),\n                            );\n                          },\n                        ),\n                );\n              } else {\n                return CustomScrollView(\n                  slivers: [\n                    HttpError(\n                      errMsg: data['msg'],\n                      fn: () => _blackListController.queryBlacklist(),\n                    )\n                  ],\n                );\n              }\n            } else {\n              // 骨架屏\n              return const SizedBox();\n            }\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass BlackListController extends GetxController {\n  int currentPage = 1;\n  int pageSize = 50;\n  RxInt total = 0.obs;\n  RxList<BlackListItem> blackList = <BlackListItem>[].obs;\n\n  Future queryBlacklist({type = 'init'}) async {\n    if (type == 'init') {\n      currentPage = 1;\n    }\n    var result = await BlackHttp.blackList(pn: currentPage, ps: pageSize);\n    if (result['status']) {\n      if (type == 'init') {\n        blackList.value = result['data'].list;\n        total.value = result['data'].total;\n      } else {\n        blackList.addAll(result['data'].list);\n      }\n\n      currentPage += 1;\n    }\n    return result;\n  }\n\n  Future removeBlack(mid) async {\n    var result = await BlackHttp.removeBlack(fid: mid);\n    if (result['status']) {\n      blackList.removeWhere((e) => e.mid == mid);\n      total.value = total.value - 1;\n      SmartDialog.showToast(result['msg']);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/danmaku/controller.dart",
    "content": "import 'package:pilipala/http/danmaku.dart';\nimport 'package:pilipala/models/danmaku/dm.pb.dart';\n\nclass PlDanmakuController {\n  PlDanmakuController(this.cid, this.type);\n  final int cid;\n  final String type;\n  Map<int, List<DanmakuElem>> dmSegMap = {};\n  // 已请求的段落标记\n  List<bool> requestedSeg = [];\n\n  bool get initiated => requestedSeg.isNotEmpty;\n\n  static int segmentLength = 60 * 6 * 1000;\n\n  void initiate(int videoDuration, int progress) {\n    if (requestedSeg.isEmpty) {\n      int segCount = (videoDuration / segmentLength).ceil();\n      requestedSeg = List<bool>.generate(segCount, (index) => false);\n    }\n    try {\n      queryDanmaku(calcSegment(progress));\n    } catch (e) {\n      print(e);\n    }\n  }\n\n  void dispose() {\n    dmSegMap.clear();\n    requestedSeg.clear();\n  }\n\n  int calcSegment(int progress) {\n    return progress ~/ segmentLength;\n  }\n\n  void queryDanmaku(int segmentIndex) async {\n    assert(requestedSeg[segmentIndex] == false);\n    if (requestedSeg.length > segmentIndex) {\n      requestedSeg[segmentIndex] = true;\n      final DmSegMobileReply result = await DanmakaHttp.queryDanmaku(\n          cid: cid, segmentIndex: segmentIndex + 1);\n      if (result.elems.isNotEmpty) {\n        for (var element in result.elems) {\n          int pos = element.progress ~/ 100; //每0.1秒存储一次\n          if (dmSegMap[pos] == null) {\n            dmSegMap[pos] = [];\n          }\n          dmSegMap[pos]!.add(element);\n        }\n      }\n    }\n  }\n\n  List<DanmakuElem>? getCurrentDanmaku(int progress) {\n    int segmentIndex = calcSegment(progress);\n    if (!requestedSeg[segmentIndex]) {\n      queryDanmaku(segmentIndex);\n    }\n    return dmSegMap[progress ~/ 100];\n  }\n}\n"
  },
  {
    "path": "lib/pages/danmaku/index.dart",
    "content": "library pldanmaku;\n\nexport './controller.dart';\nexport 'view.dart';\n"
  },
  {
    "path": "lib/pages/danmaku/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:ns_danmaku/ns_danmaku.dart';\nimport 'package:pilipala/models/danmaku/dm.pb.dart';\nimport 'package:pilipala/pages/danmaku/index.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/utils/danmaku.dart';\nimport 'package:pilipala/utils/storage.dart';\n\n/// 传入播放器控制器，监听播放进度，加载对应弹幕\nclass PlDanmaku extends StatefulWidget {\n  final int cid;\n  final PlPlayerController playerController;\n  final String type;\n  final Function(DanmakuController)? createdController;\n\n  const PlDanmaku({\n    super.key,\n    required this.cid,\n    required this.playerController,\n    this.type = 'video',\n    this.createdController,\n  });\n\n  @override\n  State<PlDanmaku> createState() => _PlDanmakuState();\n}\n\nclass _PlDanmakuState extends State<PlDanmaku> {\n  late PlPlayerController playerController;\n  late PlDanmakuController _plDanmakuController;\n  DanmakuController? _controller;\n  // bool danmuPlayStatus = true;\n  Box setting = GStrorage.setting;\n  late bool enableShowDanmaku;\n  late List blockTypes;\n  late double showArea;\n  late double opacityVal;\n  late double fontSizeVal;\n  late double danmakuDurationVal;\n  late double strokeWidth;\n  int latestAddedPosition = -1;\n\n  @override\n  void initState() {\n    super.initState();\n    enableShowDanmaku =\n        setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);\n    _plDanmakuController = PlDanmakuController(widget.cid, widget.type);\n    playerController = widget.playerController;\n    if (mounted && widget.type == 'video') {\n      if (enableShowDanmaku || playerController.isOpenDanmu.value) {\n        _plDanmakuController.initiate(\n            playerController.duration.value.inMilliseconds,\n            playerController.position.value.inMilliseconds);\n      }\n      playerController\n        ..addStatusLister(playerListener)\n        ..addPositionListener(videoPositionListen);\n    }\n    if (widget.type == 'video') {\n      playerController.isOpenDanmu.listen((p0) {\n        if (p0 && !_plDanmakuController.initiated) {\n          _plDanmakuController.initiate(\n              playerController.duration.value.inMilliseconds,\n              playerController.position.value.inMilliseconds);\n        }\n      });\n    }\n    blockTypes = playerController.blockTypes;\n    showArea = playerController.showArea;\n    opacityVal = playerController.opacityVal;\n    fontSizeVal = playerController.fontSizeVal;\n    strokeWidth = playerController.strokeWidth;\n    danmakuDurationVal = playerController.danmakuDurationVal;\n  }\n\n  // 播放器状态监听\n  void playerListener(PlayerStatus? status) {\n    if (status == PlayerStatus.paused) {\n      _controller!.pause();\n    }\n    if (status == PlayerStatus.playing) {\n      _controller!.onResume();\n    }\n  }\n\n  void videoPositionListen(Duration position) {\n    if (!playerController.isOpenDanmu.value) {\n      return;\n    }\n    int currentPosition = position.inMilliseconds;\n    currentPosition -= currentPosition % 100; //取整百的毫秒数\n\n    if (currentPosition == latestAddedPosition) {\n      return;\n    }\n    latestAddedPosition = currentPosition;\n\n    List<DanmakuElem>? currentDanmakuList =\n        _plDanmakuController.getCurrentDanmaku(currentPosition);\n\n    if (currentDanmakuList != null) {\n      Color? defaultColor = playerController.blockTypes.contains(6)\n          ? DmUtils.decimalToColor(16777215)\n          : null;\n\n      _controller!.addItems(currentDanmakuList\n          .map((e) => DanmakuItem(\n                e.content,\n                color: defaultColor ?? DmUtils.decimalToColor(e.color),\n                time: e.progress,\n                type: DmUtils.getPosition(e.mode),\n              ))\n          .toList());\n    }\n  }\n\n  @override\n  void dispose() {\n    playerController.removePositionListener(videoPositionListen);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(builder: (context, box) {\n      // double initDuration = box.maxWidth / 12;\n      return Obx(\n        () => AnimatedOpacity(\n          opacity: playerController.isOpenDanmu.value ? 1 : 0,\n          duration: const Duration(milliseconds: 100),\n          child: DanmakuView(\n            createdController: (DanmakuController e) async {\n              playerController.danmakuController = _controller = e;\n              widget.createdController?.call(e);\n            },\n            option: DanmakuOption(\n              fontSize: 15 * fontSizeVal,\n              area: showArea,\n              opacity: opacityVal,\n              hideTop: blockTypes.contains(5),\n              hideScroll: blockTypes.contains(2),\n              hideBottom: blockTypes.contains(4),\n              duration: danmakuDurationVal / playerController.playbackSpeed,\n              strokeWidth: strokeWidth,\n              // initDuration /\n              //     (danmakuSpeedVal * widget.playerController.playbackSpeed),\n            ),\n            statusChanged: (isPlaying) {},\n          ),\n        ),\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/dlna/index.dart",
    "content": "import 'dart:async';\n\nimport 'package:dlna_dart/dlna.dart';\nimport 'package:flutter/material.dart';\n\nclass LiveDlnaPage extends StatefulWidget {\n  final String datasource;\n\n  const LiveDlnaPage({Key? key, required this.datasource}) : super(key: key);\n\n  @override\n  State<LiveDlnaPage> createState() => _LiveDlnaPageState();\n}\n\nclass _LiveDlnaPageState extends State<LiveDlnaPage> {\n  final Map<String, DLNADevice> _deviceList = {};\n  final DLNAManager searcher = DLNAManager();\n  late final Timer stopSearchTimer;\n  String selectDeviceKey = '';\n  bool isSearching = true;\n\n  DLNADevice? get device => _deviceList[selectDeviceKey];\n\n  @override\n  void initState() {\n    stopSearchTimer = Timer(const Duration(seconds: 20), () {\n      setState(() => isSearching = false);\n      searcher.stop();\n    });\n    searcher.stop();\n    startSearch();\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    searcher.stop();\n    stopSearchTimer.cancel();\n  }\n\n  void startSearch() async {\n    // clear old devices\n    isSearching = true;\n    selectDeviceKey = '';\n    _deviceList.clear();\n    setState(() {});\n    // start search server\n    final m = await searcher.start();\n    m.devices.stream.listen((deviceList) {\n      deviceList.forEach((key, value) {\n        _deviceList[key] = value;\n      });\n      setState(() {});\n    });\n    // close the server, the closed server can be start by call searcher.start()\n  }\n\n  void selectDevice(String key) {\n    if (selectDeviceKey.isNotEmpty) device?.pause();\n\n    selectDeviceKey = key;\n    device?.setUrl(widget.datasource);\n    device?.play();\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget cur;\n    if (isSearching && _deviceList.isEmpty) {\n      cur = const Center(child: CircularProgressIndicator());\n    } else if (_deviceList.isEmpty) {\n      cur = Center(\n        child: Text(\n          '没有找到设备',\n          style: Theme.of(context).textTheme.bodyLarge,\n        ),\n      );\n    } else {\n      cur = ListView(\n        children: _deviceList.keys\n            .map<Widget>((key) => ListTile(\n                  contentPadding: const EdgeInsets.all(2),\n                  title: Text(_deviceList[key]!.info.friendlyName),\n                  subtitle: Text(key),\n                  onTap: () => selectDevice(key),\n                ))\n            .toList(),\n      );\n    }\n\n    return AlertDialog(\n      title: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          Text('查找设备'),\n          IconButton(\n            onPressed: startSearch,\n            icon: const Icon(Icons.refresh_rounded),\n          ),\n        ],\n      ),\n      content: SizedBox(\n        height: 200,\n        width: 200,\n        child: cur,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/controller.dart",
    "content": "// ignore_for_file: avoid_print\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/dynamics.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/common/dynamics_type.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\nimport 'package:pilipala/models/dynamics/up.dart';\nimport 'package:pilipala/models/live/item.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass DynamicsController extends GetxController {\n  int page = 1;\n  String? offset = '';\n  RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;\n  Rx<DynamicsType> dynamicsType = DynamicsType.values[0].obs;\n  RxString dynamicsTypeLabel = '全部'.obs;\n  final ScrollController scrollController = ScrollController();\n  Rx<FollowUpModel> upData = FollowUpModel().obs;\n  // 默认获取全部动态\n  RxInt mid = (-1).obs;\n  Rx<UpItem> upInfo = UpItem().obs;\n  List filterTypeList = [\n    {\n      'label': DynamicsType.all.labels,\n      'value': DynamicsType.all,\n      'enabled': true\n    },\n    {\n      'label': DynamicsType.video.labels,\n      'value': DynamicsType.video,\n      'enabled': true\n    },\n    {\n      'label': DynamicsType.pgc.labels,\n      'value': DynamicsType.pgc,\n      'enabled': true\n    },\n    {\n      'label': DynamicsType.article.labels,\n      'value': DynamicsType.article,\n      'enabled': true\n    },\n  ];\n  bool flag = false;\n  RxInt initialValue = 0.obs;\n  Box userInfoCache = GStrorage.userInfo;\n  RxBool userLogin = false.obs;\n  var userInfo;\n  RxBool isLoadingDynamic = false.obs;\n  Box setting = GStrorage.setting;\n\n  @override\n  void onInit() {\n    userInfo = userInfoCache.get('userInfoCache');\n    userLogin.value = userInfo != null;\n    super.onInit();\n    initialValue.value =\n        setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);\n    dynamicsType = DynamicsType.values[initialValue.value].obs;\n  }\n\n  Future queryFollowDynamic({type = 'init'}) async {\n    if (!userLogin.value) {\n      return {'status': false, 'msg': '账号未登录', 'code': -101};\n    }\n    if (type == 'init') {\n      dynamicsList.clear();\n    }\n    // 下拉刷新数据渲染时会触发onLoad\n    if (type == 'onLoad' && page == 1) {\n      return;\n    }\n    isLoadingDynamic.value = true;\n    var res = await DynamicsHttp.followDynamic(\n      page: type == 'init' ? 1 : page,\n      type: dynamicsType.value.values,\n      offset: offset,\n      mid: mid.value,\n    );\n    isLoadingDynamic.value = false;\n    if (res['status']) {\n      if (type == 'onLoad' && res['data'].items.isEmpty) {\n        SmartDialog.showToast('没有更多了');\n        return;\n      }\n      if (type == 'init') {\n        dynamicsList.value = res['data'].items;\n      } else {\n        dynamicsList.addAll(res['data'].items);\n      }\n      offset = res['data'].offset;\n      page++;\n    }\n    return res;\n  }\n\n  onSelectType(value) async {\n    dynamicsType.value = filterTypeList[value]['value'];\n    dynamicsList.value = <DynamicItemModel>[];\n    page = 1;\n    initialValue.value = value;\n    await queryFollowDynamic();\n    scrollController.jumpTo(0);\n  }\n\n  pushDetail(item, floor, {action = 'all'}) async {\n    feedBack();\n\n    /// 点击评论action 直接查看评论\n    if (action == 'comment') {\n      Get.toNamed('/dynamicDetail',\n          arguments: {'item': item, 'floor': floor, 'action': action});\n      return false;\n    }\n    switch (item!.type) {\n      /// 转发的动态\n      case 'DYNAMIC_TYPE_FORWARD':\n        Get.toNamed('/dynamicDetail',\n            arguments: {'item': item, 'floor': floor});\n        break;\n\n      /// 图文动态查看\n      case 'DYNAMIC_TYPE_DRAW':\n        Get.toNamed('/dynamicDetail',\n            arguments: {'item': item, 'floor': floor});\n        break;\n      case 'DYNAMIC_TYPE_AV':\n        String bvid = item.modules.moduleDynamic.major.archive.bvid;\n        String cover = item.modules.moduleDynamic.major.archive.cover;\n        try {\n          int cid = await SearchHttp.ab2c(bvid: bvid);\n          Get.toNamed('/video?bvid=$bvid&cid=$cid',\n              arguments: {'pic': cover, 'heroTag': bvid});\n        } catch (err) {\n          SmartDialog.showToast(err.toString());\n        }\n        break;\n\n      /// 专栏文章查看\n      case 'DYNAMIC_TYPE_ARTICLE':\n        String title = item.modules.moduleDynamic.major.opus.title;\n        String jumpUrl = item.modules.moduleDynamic.major.opus.jumpUrl;\n        String url =\n            jumpUrl.startsWith('//') ? jumpUrl.split('//').last : jumpUrl;\n        if (jumpUrl.contains('opus') || jumpUrl.contains('read')) {\n          RegExp digitRegExp = RegExp(r'\\d+');\n          Iterable<Match> matches = digitRegExp.allMatches(jumpUrl);\n          String number = matches.first.group(0)!;\n          if (jumpUrl.contains('read')) {\n            Get.toNamed('/read', parameters: {\n              'title': title,\n              'id': number,\n              'articleType': url.split('/')[1]\n            });\n          } else {\n            Get.toNamed('/opus', parameters: {\n              'title': title,\n              'id': number,\n              'articleType': 'opus'\n            });\n          }\n        } else {\n          Get.toNamed(\n            '/webview',\n            parameters: {\n              'url': 'https:$url',\n              'type': 'note',\n              'pageTitle': title\n            },\n          );\n        }\n\n        break;\n      case 'DYNAMIC_TYPE_PGC':\n        print('番剧');\n        SmartDialog.showToast('暂未支持的类型，请联系开发者');\n        break;\n\n      /// 纯文字动态查看\n      case 'DYNAMIC_TYPE_WORD':\n        print('纯文本');\n        Get.toNamed('/dynamicDetail',\n            arguments: {'item': item, 'floor': floor});\n        break;\n      case 'DYNAMIC_TYPE_LIVE_RCMD':\n        DynamicLiveModel liveRcmd = item.modules.moduleDynamic.major.liveRcmd;\n        ModuleAuthorModel author = item.modules.moduleAuthor;\n        LiveItemModel liveItem = LiveItemModel.fromJson({\n          'title': liveRcmd.title,\n          'uname': author.name,\n          'cover': liveRcmd.cover,\n          'mid': author.mid,\n          'face': author.face,\n          'roomid': liveRcmd.roomId,\n          'watched_show': liveRcmd.watchedShow,\n        });\n        Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', arguments: {\n          'liveItem': liveItem,\n          'heroTag': liveItem.roomId.toString()\n        });\n        break;\n\n      /// 合集查看\n      case 'DYNAMIC_TYPE_UGC_SEASON':\n        DynamicArchiveModel ugcSeason =\n            item.modules.moduleDynamic.major.ugcSeason;\n        int aid = ugcSeason.aid!;\n        String bvid = IdUtils.av2bv(aid);\n        String cover = ugcSeason.cover!;\n        int cid = await SearchHttp.ab2c(bvid: bvid);\n        Get.toNamed('/video?bvid=$bvid&cid=$cid',\n            arguments: {'pic': cover, 'heroTag': bvid});\n        break;\n\n      /// 番剧查看\n      case 'DYNAMIC_TYPE_PGC_UNION':\n        print('DYNAMIC_TYPE_PGC_UNION 番剧');\n        DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;\n        if (pgc.epid != null) {\n          RoutePush.bangumiPush(null, pgc.epid);\n        }\n        break;\n    }\n  }\n\n  Future queryFollowUp({type = 'init'}) async {\n    if (!userLogin.value) {\n      return {'status': false, 'msg': '账号未登录', 'code': -101};\n    }\n    if (type == 'init') {\n      upData.value.upList = <UpItem>[];\n      upData.value.liveList = <LiveUserItem>[];\n    }\n    var res = await DynamicsHttp.followUp();\n    if (res['status']) {\n      upData.value = res['data'];\n      if (upData.value.upList!.isEmpty) {\n        mid.value = -1;\n      }\n      upData.value.upList!.insertAll(0, [\n        UpItem(face: '', uname: '全部动态', mid: -1),\n        UpItem(face: userInfo.face, uname: '我', mid: userInfo.mid),\n      ]);\n    }\n    return res;\n  }\n\n  onSelectUp(mid) async {\n    dynamicsType.value = DynamicsType.values[0];\n    dynamicsList.value = <DynamicItemModel>[];\n    page = 1;\n    queryFollowDynamic();\n  }\n\n  onRefresh() async {\n    page = 1;\n    await queryFollowUp();\n    await queryFollowDynamic();\n  }\n\n  // 返回顶部并刷新\n  void animateToTop() async {\n    if (scrollController.offset >=\n        MediaQuery.of(Get.context!).size.height * 5) {\n      scrollController.jumpTo(0);\n    } else {\n      await scrollController.animateTo(0,\n          duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);\n    }\n  }\n\n  // 重置搜索\n  void resetSearch() {\n    mid.value = -1;\n    dynamicsType.value = DynamicsType.values[0];\n    initialValue.value = 0;\n    SmartDialog.showToast('还原默认加载');\n    dynamicsList.value = <DynamicItemModel>[];\n    queryFollowDynamic();\n  }\n\n  // 点击up主\n  void onTapUp(data) {\n    mid.value = data.mid;\n    upInfo.value = data;\n    onSelectUp(data.mid);\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/detail/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/html.dart';\nimport 'package:pilipala/http/reply.dart';\nimport 'package:pilipala/models/common/reply_sort_type.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass DynamicDetailController extends GetxController {\n  DynamicDetailController(this.oid, this.type);\n  int? oid;\n  int? type;\n  dynamic item;\n  int? floor;\n  int currentPage = 0;\n  bool isLoadingMore = false;\n  RxString noMore = ''.obs;\n  RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;\n  RxInt acount = 0.obs;\n  final ScrollController scrollController = ScrollController();\n\n  ReplySortType _sortType = ReplySortType.time;\n  RxString sortTypeTitle = ReplySortType.time.titles.obs;\n  RxString sortTypeLabel = ReplySortType.time.labels.obs;\n  Box setting = GStrorage.setting;\n  RxInt replyReqCode = 200.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    item = Get.arguments['item'];\n    floor = Get.arguments['floor'];\n    if (floor == 1) {\n      acount.value =\n          int.parse(item!.modules!.moduleStat!.comment!.count ?? '0');\n    }\n    int deaultReplySortIndex =\n        setting.get(SettingBoxKey.replySortType, defaultValue: 0);\n    if (deaultReplySortIndex == 2) {\n      setting.put(SettingBoxKey.replySortType, 0);\n      deaultReplySortIndex = 0;\n    }\n    _sortType = ReplySortType.values[deaultReplySortIndex];\n    sortTypeTitle.value = _sortType.titles;\n    sortTypeLabel.value = _sortType.labels;\n  }\n\n  Future queryReplyList({reqType = 'init'}) async {\n    if (reqType == 'init') {\n      currentPage = 0;\n    }\n    var res = await ReplyHttp.replyList(\n      oid: oid!,\n      pageNum: currentPage + 1,\n      type: type!,\n      sort: _sortType.index,\n    );\n    if (res['status']) {\n      List<ReplyItemModel> replies = res['data'].replies;\n      acount.value = res['data'].page.acount;\n      if (replies.isNotEmpty) {\n        currentPage++;\n        noMore.value = '加载中...';\n        if (replies.length < 20) {\n          noMore.value = '没有更多了';\n        }\n      } else {\n        noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';\n      }\n      if (reqType == 'init') {\n        // 添加置顶回复\n        if (res['data'].upper.top != null) {\n          bool flag = res['data']\n              .topReplies\n              .any((reply) => reply.rpid == res['data'].upper.top.rpid);\n          if (!flag) {\n            replies.insert(0, res['data'].upper.top);\n          }\n        }\n        replies.insertAll(0, res['data'].topReplies);\n        replyList.value = replies;\n      } else {\n        replyList.addAll(replies);\n      }\n    }\n    replyReqCode.value = res['code'];\n    isLoadingMore = false;\n    return res;\n  }\n\n  // 排序搜索评论\n  queryBySort() {\n    feedBack();\n    switch (_sortType) {\n      case ReplySortType.time:\n        _sortType = ReplySortType.like;\n        break;\n      case ReplySortType.like:\n        _sortType = ReplySortType.time;\n        break;\n      default:\n    }\n    sortTypeTitle.value = _sortType.titles;\n    sortTypeLabel.value = _sortType.labels;\n    replyList.clear();\n    queryReplyList(reqType: 'init');\n  }\n\n  // 根据jumpUrl获取动态html\n  reqHtmlByOpusId(int id) async {\n    var res = await HtmlHttp.reqHtml(id, 'opus');\n    oid = res['commentId'];\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/detail/index.dart",
    "content": "library dynamic_detail;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/dynamics/detail/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_reply.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\nimport 'package:pilipala/pages/dynamics/detail/index.dart';\nimport 'package:pilipala/pages/dynamics/widgets/author_panel.dart';\nimport 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';\nimport 'package:pilipala/pages/video/detail/reply_new/index.dart';\nimport 'package:pilipala/pages/video/detail/reply_reply/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/id_utils.dart';\n\nimport '../../../models/video/reply/item.dart';\nimport '../widgets/dynamic_panel.dart';\n\nclass DynamicDetailPage extends StatefulWidget {\n  // const DynamicDetailPage({super.key});\n  const DynamicDetailPage({Key? key}) : super(key: key);\n\n  @override\n  State<DynamicDetailPage> createState() => _DynamicDetailPageState();\n}\n\nclass _DynamicDetailPageState extends State<DynamicDetailPage>\n    with TickerProviderStateMixin {\n  late DynamicDetailController _dynamicDetailController;\n  late AnimationController fabAnimationCtr;\n  Future? _futureBuilderFuture;\n  late StreamController<bool> titleStreamC =\n      StreamController<bool>.broadcast(); // appBar title\n  late ScrollController scrollController;\n  bool _visibleTitle = false;\n  String? action;\n  // 回复类型\n  late int replyType;\n  bool _isFabVisible = true;\n  int oid = 0;\n  int? opusId;\n  bool isOpusId = false;\n\n  @override\n  void initState() {\n    super.initState();\n    // floor 1原创 2转发\n    init();\n    if (action == 'comment') {\n      _visibleTitle = true;\n      titleStreamC.add(true);\n    }\n\n    fabAnimationCtr = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n    );\n    fabAnimationCtr.forward();\n    // 滚动事件监听\n    scrollListener();\n  }\n\n  // 页面初始化\n  void init() async {\n    Map args = Get.arguments;\n    // 楼层\n    int floor = args['floor'];\n    // 从action栏点击进入\n    action = args.containsKey('action') ? args['action'] : null;\n    // 评论类型\n    int commentType = args['item'].basic!['comment_type'] ?? 11;\n    replyType = (commentType == 0) ? 11 : commentType;\n\n    if (floor == 1) {\n      oid = int.parse(args['item'].basic!['comment_id_str']);\n    } else {\n      try {\n        ModuleDynamicModel moduleDynamic = args['item'].modules.moduleDynamic;\n        String majorType = moduleDynamic.major!.type!;\n\n        if (majorType == 'MAJOR_TYPE_OPUS') {\n          // 转发的动态\n          String jumpUrl = moduleDynamic.major!.opus!.jumpUrl!;\n          opusId = int.parse(jumpUrl.split('/').last);\n          if (opusId != null) {\n            isOpusId = true;\n            _dynamicDetailController = Get.put(\n                DynamicDetailController(oid, replyType),\n                tag: opusId.toString());\n            await _dynamicDetailController.reqHtmlByOpusId(opusId!);\n            setState(() {});\n          }\n        } else {\n          oid = moduleDynamic.major!.draw!.id!;\n        }\n      } catch (_) {}\n    }\n    if (!isOpusId) {\n      _dynamicDetailController =\n          Get.put(DynamicDetailController(oid, replyType), tag: oid.toString());\n    }\n    _futureBuilderFuture = _dynamicDetailController.queryReplyList();\n  }\n\n  // 查看二级评论\n  void replyReply(replyItem, currentReply, loadMore) {\n    int oid = replyItem.oid;\n    int rpid = replyItem.rpid!;\n    Get.to(\n      () => Scaffold(\n        appBar: AppBar(\n          titleSpacing: 0,\n          centerTitle: false,\n          title: Text(\n            '评论详情',\n            style: Theme.of(context).textTheme.titleMedium,\n          ),\n        ),\n        body: VideoReplyReplyPanel(\n          oid: oid,\n          rpid: rpid,\n          source: 'dynamic',\n          replyType: ReplyType.values[replyType],\n          firstFloor: replyItem,\n          loadMore: loadMore,\n        ),\n      ),\n    );\n  }\n\n  // 滑动事件监听\n  void scrollListener() {\n    scrollController = _dynamicDetailController.scrollController;\n    scrollController.addListener(\n      () {\n        // 分页加载\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {\n            _dynamicDetailController.queryReplyList(reqType: 'onLoad');\n          });\n        }\n\n        // 标题\n        if (scrollController.offset > 55 && !_visibleTitle) {\n          _visibleTitle = true;\n          titleStreamC.add(true);\n        } else if (scrollController.offset <= 55 && _visibleTitle) {\n          _visibleTitle = false;\n          titleStreamC.add(false);\n        }\n\n        // fab按钮\n        final ScrollDirection direction =\n            scrollController.position.userScrollDirection;\n        if (direction == ScrollDirection.forward) {\n          _showFab();\n        } else if (direction == ScrollDirection.reverse) {\n          _hideFab();\n        }\n      },\n    );\n  }\n\n  void _showFab() {\n    if (!_isFabVisible) {\n      _isFabVisible = true;\n      fabAnimationCtr.forward();\n    }\n  }\n\n  void _hideFab() {\n    if (_isFabVisible) {\n      _isFabVisible = false;\n      fabAnimationCtr.reverse();\n    }\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    fabAnimationCtr.dispose();\n    scrollController.dispose();\n    titleStreamC.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        elevation: 0,\n        scrolledUnderElevation: 1,\n        centerTitle: false,\n        titleSpacing: 0,\n        title: StreamBuilder(\n          stream: titleStreamC.stream,\n          initialData: false,\n          builder: (context, AsyncSnapshot snapshot) {\n            return AnimatedOpacity(\n              opacity: snapshot.data ? 1 : 0,\n              duration: const Duration(milliseconds: 300),\n              child: AuthorPanel(item: _dynamicDetailController.item),\n            );\n          },\n        ),\n        // actions: _detailModel != null ? appBarAction() : [],\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async {\n          await _dynamicDetailController.queryReplyList();\n        },\n        child: CustomScrollView(\n          controller: scrollController,\n          slivers: [\n            if (action != 'comment')\n              SliverToBoxAdapter(\n                child: DynamicPanel(\n                  item: _dynamicDetailController.item,\n                  source: 'detail',\n                ),\n              ),\n            SliverPersistentHeader(\n              delegate: _MySliverPersistentHeaderDelegate(\n                child: Container(\n                  decoration: BoxDecoration(\n                    color: Theme.of(context).colorScheme.surface,\n                    border: Border(\n                      top: BorderSide(\n                        width: 0.6,\n                        color: Theme.of(context).dividerColor.withOpacity(0.05),\n                      ),\n                    ),\n                  ),\n                  height: 45,\n                  padding: const EdgeInsets.only(left: 12, right: 6),\n                  child: Row(\n                    children: [\n                      Obx(\n                        () => AnimatedSwitcher(\n                          duration: const Duration(milliseconds: 400),\n                          transitionBuilder:\n                              (Widget child, Animation<double> animation) {\n                            return ScaleTransition(\n                                scale: animation, child: child);\n                          },\n                          child: Text(\n                            '${_dynamicDetailController.acount.value}',\n                            key: ValueKey<int>(\n                                _dynamicDetailController.acount.value),\n                          ),\n                        ),\n                      ),\n                      const Text('条回复'),\n                      const Spacer(),\n                      SizedBox(\n                        height: 35,\n                        child: TextButton.icon(\n                          onPressed: () =>\n                              _dynamicDetailController.queryBySort(),\n                          icon: const Icon(Icons.sort, size: 16),\n                          label: Obx(() => Text(\n                                _dynamicDetailController.sortTypeLabel.value,\n                                style: const TextStyle(fontSize: 13),\n                              )),\n                        ),\n                      )\n                    ],\n                  ),\n                ),\n              ),\n              pinned: true,\n            ),\n            FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  Map data = snapshot.data as Map;\n                  if (snapshot.data['status']) {\n                    RxList<ReplyItemModel> replyList =\n                        _dynamicDetailController.replyList;\n                    // 请求成功\n                    return Obx(\n                      () => replyList.isEmpty &&\n                              _dynamicDetailController.isLoadingMore\n                          ? SliverList(\n                              delegate:\n                                  SliverChildBuilderDelegate((context, index) {\n                                return const VideoReplySkeleton();\n                              }, childCount: 8),\n                            )\n                          : SliverList(\n                              delegate: SliverChildBuilderDelegate(\n                                (context, index) {\n                                  if (index == replyList.length) {\n                                    return Container(\n                                      padding: EdgeInsets.only(\n                                          bottom: MediaQuery.of(context)\n                                              .padding\n                                              .bottom),\n                                      height: MediaQuery.of(context)\n                                              .padding\n                                              .bottom +\n                                          100,\n                                      child: Center(\n                                        child: Obx(\n                                          () => Text(\n                                            _dynamicDetailController\n                                                .noMore.value,\n                                            style: TextStyle(\n                                              fontSize: 12,\n                                              color: Theme.of(context)\n                                                  .colorScheme\n                                                  .outline,\n                                            ),\n                                          ),\n                                        ),\n                                      ),\n                                    );\n                                  } else {\n                                    return ReplyItem(\n                                      replyItem: replyList[index],\n                                      showReplyRow: true,\n                                      replyLevel: '1',\n                                      replyReply:\n                                          (replyItem, currentReply, loadMore) =>\n                                              replyReply(replyItem,\n                                                  currentReply, loadMore),\n                                      replyType: ReplyType.values[replyType],\n                                      addReply: (replyItem) {\n                                        replyList[index]\n                                            .replies!\n                                            .add(replyItem);\n                                      },\n                                    );\n                                  }\n                                },\n                                childCount: replyList.length + 1,\n                              ),\n                            ),\n                    );\n                  } else {\n                    // 请求错误\n                    return HttpError(\n                      errMsg: data['msg'],\n                      fn: () => setState(() {}),\n                    );\n                  }\n                } else {\n                  // 骨架屏\n                  return SliverList(\n                    delegate: SliverChildBuilderDelegate((context, index) {\n                      return const VideoReplySkeleton();\n                    }, childCount: 8),\n                  );\n                }\n              },\n            )\n          ],\n        ),\n      ),\n      floatingActionButton: SlideTransition(\n        position: Tween<Offset>(\n          begin: const Offset(0, 2),\n          end: const Offset(0, 0),\n        ).animate(\n          CurvedAnimation(\n            parent: fabAnimationCtr,\n            curve: Curves.easeInOut,\n          ),\n        ),\n        child: Obx(\n          () => _dynamicDetailController.replyReqCode.value == 12061\n              ? const SizedBox()\n              : FloatingActionButton(\n                  heroTag: null,\n                  onPressed: () {\n                    feedBack();\n                    showModalBottomSheet(\n                      context: context,\n                      isScrollControlled: true,\n                      builder: (BuildContext context) {\n                        return VideoReplyNewDialog(\n                          oid: _dynamicDetailController.oid ??\n                              IdUtils.bv2av(Get.parameters['bvid']!),\n                          root: 0,\n                          parent: 0,\n                          replyType: ReplyType.values[replyType],\n                        );\n                      },\n                    ).then(\n                      (value) => {\n                        // 完成评论，数据添加\n                        if (value != null && value['data'] != null)\n                          {\n                            _dynamicDetailController.replyList\n                                .add(value['data']),\n                            _dynamicDetailController.acount.value++\n                          }\n                      },\n                    );\n                  },\n                  tooltip: '评论动态',\n                  child: const Icon(Icons.reply),\n                ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {\n  final double _minExtent = 45;\n  final double _maxExtent = 45;\n  final Widget child;\n\n  _MySliverPersistentHeaderDelegate({required this.child});\n\n  @override\n  Widget build(\n      BuildContext context, double shrinkOffset, bool overlapsContent) {\n    //创建child子组件\n    //shrinkOffset：child偏移值minExtent~maxExtent\n    //overlapsContent：SliverPersistentHeader覆盖其他子组件返回true，否则返回false\n    return child;\n  }\n\n  //SliverPersistentHeader最大高度\n  @override\n  double get maxExtent => _maxExtent;\n\n  //SliverPersistentHeader最小高度\n  @override\n  double get minExtent => _minExtent;\n\n  @override\n  bool shouldRebuild(covariant _MySliverPersistentHeaderDelegate oldDelegate) {\n    return true;\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/index.dart",
    "content": "library dynamics;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/dynamics/up_dynamic/controller.dart",
    "content": "import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/dynamics.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\nimport 'package:pilipala/models/dynamics/up.dart';\n\nclass UpDynamicsController extends GetxController {\n  UpDynamicsController(this.upInfo);\n  UpItem upInfo;\n  RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;\n  RxBool isLoadingDynamic = false.obs;\n  String? offset = '';\n  int page = 1;\n\n  Future queryFollowDynamic({type = 'init'}) async {\n    if (type == 'init') {\n      dynamicsList.clear();\n    }\n    // 下拉刷新数据渲染时会触发onLoad\n    if (type == 'onLoad' && page == 1) {\n      return;\n    }\n    isLoadingDynamic.value = true;\n    var res = await DynamicsHttp.followDynamic(\n      page: type == 'init' ? 1 : page,\n      type: 'all',\n      offset: offset,\n      mid: upInfo.mid,\n    );\n    isLoadingDynamic.value = false;\n    if (res['status']) {\n      if (type == 'onLoad' && res['data'].items.isEmpty) {\n        SmartDialog.showToast('没有更多了');\n        return;\n      }\n      if (type == 'init') {\n        dynamicsList.value = res['data'].items;\n      } else {\n        dynamicsList.addAll(res['data'].items);\n      }\n      offset = res['data'].offset;\n      page++;\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/up_dynamic/index.dart",
    "content": "library up_dynamics;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/dynamics/up_dynamic/route_panel.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/dynamics/up.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport '../controller.dart';\nimport 'index.dart';\n\nclass OverlayPanel extends StatefulWidget {\n  const OverlayPanel({super.key, required this.ctr, required this.upInfo});\n\n  final DynamicsController ctr;\n  final UpItem upInfo;\n\n  @override\n  State<OverlayPanel> createState() => _OverlayPanelState();\n}\n\nclass _OverlayPanelState extends State<OverlayPanel>\n    with SingleTickerProviderStateMixin {\n  static const itemPadding = EdgeInsets.symmetric(horizontal: 6, vertical: 0);\n  final PageController pageController = PageController();\n  late double contentWidth = 50;\n  late List<UpItem> upList;\n  late RxInt currentMid = (-1).obs;\n  TabController? _tabController;\n\n  @override\n  void initState() {\n    super.initState();\n    upList = widget.ctr.upData.value.upList!\n        .map<UpItem>((element) => element)\n        .toList();\n    upList.removeAt(0);\n    _tabController = TabController(length: upList.length, vsync: this);\n\n    currentMid.value = widget.upInfo.mid!;\n\n    pageController.addListener(() {\n      int index = pageController.page!.round();\n      int mid = upList[index].mid!;\n      if (mid != currentMid.value) {\n        currentMid.value = mid;\n        _tabController?.animateTo(index,\n            duration: Duration.zero, curve: Curves.linear);\n        onClickUp(upList[index], index, type: 'pageChange');\n      }\n    });\n\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      int index =\n          upList.indexWhere((element) => element.mid == widget.upInfo.mid);\n      pageController.jumpToPage(index);\n      onClickUp(widget.upInfo, index);\n      _tabController?.animateTo(index,\n          duration: Duration.zero, curve: Curves.linear);\n      onClickUp(upList[index], index, type: 'pageChange');\n    });\n  }\n\n  void onClickUp(data, i, {type = 'click'}) {\n    if (type == 'click') {\n      pageController.jumpToPage(i);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: Get.width,\n      height: Get.height,\n      clipBehavior: Clip.antiAlias,\n      margin: EdgeInsets.fromLTRB(\n        0,\n        MediaQuery.of(context).padding.top + 4,\n        0,\n        MediaQuery.of(context).padding.bottom + 4,\n      ),\n      decoration: BoxDecoration(\n        color: Colors.transparent,\n        borderRadius: BorderRadius.circular(20),\n      ),\n      child: Column(\n        children: [\n          SizedBox(\n            height: 50,\n            child: TabBar(\n              controller: _tabController,\n              dividerColor: Colors.transparent,\n              automaticIndicatorColorAdjustment: false,\n              tabAlignment: TabAlignment.start,\n              padding: const EdgeInsets.only(left: 12, right: 12),\n              indicatorPadding: EdgeInsets.zero,\n              indicatorSize: TabBarIndicatorSize.label,\n              indicator: const BoxDecoration(),\n              labelPadding: itemPadding,\n              indicatorWeight: 1,\n              isScrollable: true,\n              tabs: upList.map((e) => Tab(child: upItemBuild(e))).toList(),\n              onTap: (index) {\n                feedBack();\n                EasyThrottle.throttle(\n                    'follow', const Duration(milliseconds: 200), () {\n                  onClickUp(upList[index], index);\n                });\n              },\n            ),\n          ),\n          Expanded(\n            child: PageView.builder(\n              itemCount: upList.length,\n              controller: pageController,\n              itemBuilder: (BuildContext context, int index) {\n                return Container(\n                  clipBehavior: Clip.antiAlias,\n                  margin: const EdgeInsets.fromLTRB(10, 12, 10, 0),\n                  decoration: BoxDecoration(\n                    color: Theme.of(context).colorScheme.surface,\n                    borderRadius: BorderRadius.circular(20),\n                  ),\n                  child: UpDyanmicsPage(upInfo: upList[index], ctr: widget.ctr),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget upItemBuild(data) {\n    return Obx(\n      () => AnimatedOpacity(\n        opacity: currentMid == data.mid ? 1 : 0.3,\n        duration: const Duration(milliseconds: 200),\n        curve: Curves.easeInOut,\n        child: AnimatedScale(\n          duration: const Duration(milliseconds: 200),\n          scale: currentMid == data.mid ? 1 : 0.9,\n          child: NetworkImgLayer(\n            width: contentWidth,\n            height: contentWidth,\n            src: data.face,\n            type: 'avatar',\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/up_dynamic/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/dynamic_card.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\nimport 'package:pilipala/models/dynamics/up.dart';\nimport 'package:pilipala/pages/dynamics/up_dynamic/index.dart';\n\nimport '../index.dart';\nimport '../widgets/dynamic_panel.dart';\n\nclass UpDyanmicsPage extends StatefulWidget {\n  final UpItem upInfo;\n  final DynamicsController ctr;\n\n  const UpDyanmicsPage({\n    required this.upInfo,\n    required this.ctr,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  State<UpDyanmicsPage> createState() => _UpDyanmicsPageState();\n}\n\nclass _UpDyanmicsPageState extends State<UpDyanmicsPage>\n    with AutomaticKeepAliveClientMixin {\n  late UpDynamicsController _upDynamicsController;\n  final ScrollController scrollController = ScrollController();\n  late Future _futureBuilderFuture;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    _upDynamicsController = Get.put(UpDynamicsController(widget.upInfo),\n        tag: widget.upInfo.mid.toString());\n    _futureBuilderFuture = _upDynamicsController.queryFollowDynamic();\n\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle(\n              'queryFollowDynamic', const Duration(seconds: 1), () {\n            _upDynamicsController.queryFollowDynamic(type: 'onLoad');\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return CustomScrollView(\n      controller: scrollController,\n      physics: const AlwaysScrollableScrollPhysics(),\n      slivers: [\n        SliverPersistentHeader(\n          pinned: true,\n          floating: true,\n          delegate: _MySliverPersistentHeaderDelegate(\n            child: Container(\n              height: 50,\n              padding: const EdgeInsets.fromLTRB(20, 4, 4, 4),\n              decoration: BoxDecoration(\n                color: Theme.of(context).colorScheme.surface,\n                border: Border(\n                  bottom: BorderSide(\n                    color: Theme.of(context).colorScheme.onSurface,\n                    width: 0.1,\n                  ),\n                ),\n              ),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                children: [\n                  Text(\n                    widget.upInfo.uname!,\n                    style: Theme.of(context)\n                        .textTheme\n                        .titleMedium!\n                        .copyWith(fontWeight: FontWeight.bold),\n                  ),\n                  IconButton(\n                    onPressed: () => Navigator.of(context).pop(),\n                    icon: const Icon(Icons.close),\n                  )\n                ],\n              ),\n            ),\n          ),\n        ),\n        FutureBuilder(\n          future: _futureBuilderFuture,\n          builder: (context, snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              if (snapshot.data == null) {\n                return const SliverToBoxAdapter(child: SizedBox());\n              }\n              Map? data = snapshot.data;\n              if (data != null && data['status']) {\n                List<DynamicItemModel> list =\n                    _upDynamicsController.dynamicsList;\n                return Obx(\n                  () {\n                    if (list.isEmpty) {\n                      if (_upDynamicsController.isLoadingDynamic.value) {\n                        return skeleton();\n                      } else {\n                        return const NoData();\n                      }\n                    } else {\n                      return SliverList(\n                        delegate: SliverChildBuilderDelegate(\n                          (context, index) {\n                            return DynamicPanel(item: list[index]);\n                          },\n                          childCount: list.length,\n                        ),\n                      );\n                    }\n                  },\n                );\n              } else {\n                return HttpError(\n                  errMsg: data?['msg'] ?? '请求异常',\n                  btnText: data?['code'] == -101 ? '去登录' : null,\n                  fn: () {},\n                );\n              }\n            } else {\n              // 骨架屏\n              return skeleton();\n            }\n          },\n        ),\n      ],\n    );\n  }\n\n  Widget skeleton() {\n    return SliverList(\n      delegate: SliverChildBuilderDelegate((context, index) {\n        return const DynamicCardSkeleton();\n      }, childCount: 5),\n    );\n  }\n}\n\nclass _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {\n  _MySliverPersistentHeaderDelegate({required this.child});\n  final double _minExtent = 50;\n  final double _maxExtent = 50;\n  final Widget child;\n\n  @override\n  Widget build(\n      BuildContext context, double shrinkOffset, bool overlapsContent) {\n    return child;\n  }\n\n  @override\n  double get maxExtent => _maxExtent;\n\n  @override\n  double get minExtent => _minExtent;\n\n  @override\n  bool shouldRebuild(covariant _MySliverPersistentHeaderDelegate oldDelegate) {\n    return true;\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/skeleton/dynamic_card.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\nimport 'package:pilipala/plugin/pl_popup/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/main_stream.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nimport '../mine/controller.dart';\nimport 'controller.dart';\nimport 'widgets/dynamic_panel.dart';\nimport 'up_dynamic/route_panel.dart';\nimport 'widgets/up_panel.dart';\n\nclass DynamicsPage extends StatefulWidget {\n  const DynamicsPage({super.key});\n\n  @override\n  State<DynamicsPage> createState() => _DynamicsPageState();\n}\n\nclass _DynamicsPageState extends State<DynamicsPage>\n    with AutomaticKeepAliveClientMixin {\n  final DynamicsController _dynamicsController = Get.put(DynamicsController());\n  final MineController mineController = Get.put(MineController());\n  late Future _futureBuilderFuture;\n  late Future _futureBuilderFutureUp;\n  Box userInfoCache = GStrorage.userInfo;\n  late ScrollController scrollController;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _dynamicsController.queryFollowDynamic();\n    _futureBuilderFutureUp = _dynamicsController.queryFollowUp();\n    scrollController = _dynamicsController.scrollController;\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle(\n              'queryFollowDynamic', const Duration(seconds: 1), () {\n            _dynamicsController.queryFollowDynamic(type: 'onLoad');\n          });\n        }\n        handleScrollEvent(scrollController);\n      },\n    );\n\n    _dynamicsController.userLogin.listen((status) {\n      if (mounted) {\n        setState(() {\n          _futureBuilderFuture = _dynamicsController.queryFollowDynamic();\n          _futureBuilderFutureUp = _dynamicsController.queryFollowUp();\n        });\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return Scaffold(\n      appBar: AppBar(\n        elevation: 0,\n        scrolledUnderElevation: 0,\n        title: SizedBox(\n          height: 34,\n          child: Stack(\n            children: [\n              Row(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  Obx(() {\n                    if (_dynamicsController.mid.value != -1 &&\n                        _dynamicsController.upInfo.value.uname != null) {\n                      return SizedBox(\n                        height: 36,\n                        child: AnimatedSwitcher(\n                          duration: const Duration(milliseconds: 300),\n                          transitionBuilder:\n                              (Widget child, Animation<double> animation) {\n                            return ScaleTransition(\n                                scale: animation, child: child);\n                          },\n                          child: Text(\n                              '${_dynamicsController.upInfo.value.uname!}的动态',\n                              key: ValueKey<String>(\n                                  _dynamicsController.upInfo.value.uname!),\n                              style: TextStyle(\n                                fontSize: Theme.of(context)\n                                    .textTheme\n                                    .labelLarge!\n                                    .fontSize,\n                              )),\n                        ),\n                      );\n                    } else {\n                      return const SizedBox();\n                    }\n                  }),\n                  Obx(\n                    () => _dynamicsController.userLogin.value\n                        ? Visibility(\n                            visible: _dynamicsController.mid.value == -1,\n                            child: Theme(\n                              data: ThemeData(\n                                splashColor:\n                                    Colors.transparent, // 点击时的水波纹颜色设置为透明\n                                highlightColor:\n                                    Colors.transparent, // 点击时的背景高亮颜色设置为透明\n                              ),\n                              child: CustomSlidingSegmentedControl<int>(\n                                initialValue:\n                                    _dynamicsController.initialValue.value,\n                                children: {\n                                  0: Text(\n                                    '全部',\n                                    style: TextStyle(\n                                        fontSize: Theme.of(context)\n                                            .textTheme\n                                            .labelMedium!\n                                            .fontSize),\n                                  ),\n                                  1: Text('投稿',\n                                      style: TextStyle(\n                                          fontSize: Theme.of(context)\n                                              .textTheme\n                                              .labelMedium!\n                                              .fontSize)),\n                                  2: Text('番剧',\n                                      style: TextStyle(\n                                          fontSize: Theme.of(context)\n                                              .textTheme\n                                              .labelMedium!\n                                              .fontSize)),\n                                  3: Text('专栏',\n                                      style: TextStyle(\n                                          fontSize: Theme.of(context)\n                                              .textTheme\n                                              .labelMedium!\n                                              .fontSize)),\n                                },\n                                padding: 13.0,\n                                decoration: BoxDecoration(\n                                  color: Theme.of(context)\n                                      .colorScheme\n                                      .surfaceVariant\n                                      .withOpacity(0.7),\n                                  borderRadius: BorderRadius.circular(20),\n                                ),\n                                thumbDecoration: BoxDecoration(\n                                  color: Theme.of(context).colorScheme.surface,\n                                  borderRadius: BorderRadius.circular(20),\n                                ),\n                                duration: const Duration(milliseconds: 300),\n                                curve: Curves.easeInOut,\n                                onValueChanged: (v) {\n                                  feedBack();\n                                  _dynamicsController.onSelectType(v);\n                                },\n                              ),\n                            ),\n                          )\n                        : Text('动态',\n                            style: Theme.of(context).textTheme.titleMedium),\n                  )\n                ],\n              ),\n            ],\n          ),\n        ),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () => _dynamicsController.onRefresh(),\n        child: CustomScrollView(\n          controller: _dynamicsController.scrollController,\n          slivers: [\n            FutureBuilder(\n              future: _futureBuilderFutureUp,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  if (snapshot.data == null) {\n                    return const SliverToBoxAdapter(child: SizedBox());\n                  }\n                  Map data = snapshot.data;\n                  if (data['status']) {\n                    return Obx(\n                      () => UpPanel(\n                        upData: _dynamicsController.upData.value,\n                        onClickUpCb: (data) {\n                          // _dynamicsController.onTapUp(data);\n                          Navigator.push(\n                            context,\n                            PlPopupRoute(\n                              child: OverlayPanel(\n                                  ctr: _dynamicsController, upInfo: data),\n                            ),\n                          );\n                        },\n                      ),\n                    );\n                  } else {\n                    return const SliverToBoxAdapter(\n                      child: SizedBox(height: 80),\n                    );\n                  }\n                } else {\n                  return const SliverToBoxAdapter(\n                      child: SizedBox(\n                    height: 90,\n                    child: UpPanelSkeleton(),\n                  ));\n                }\n              },\n            ),\n            FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  if (snapshot.data == null) {\n                    return const SliverToBoxAdapter(child: SizedBox());\n                  }\n                  Map? data = snapshot.data;\n                  if (data != null && data['status']) {\n                    List<DynamicItemModel> list =\n                        _dynamicsController.dynamicsList;\n                    return Obx(\n                      () {\n                        if (list.isEmpty) {\n                          if (_dynamicsController.isLoadingDynamic.value) {\n                            return skeleton();\n                          } else {\n                            return const NoData();\n                          }\n                        } else {\n                          return SliverList(\n                            delegate: SliverChildBuilderDelegate(\n                              (context, index) {\n                                return DynamicPanel(item: list[index]);\n                              },\n                              childCount: list.length,\n                            ),\n                          );\n                        }\n                      },\n                    );\n                  } else {\n                    return HttpError(\n                      errMsg: data?['msg'] ?? '请求异常',\n                      btnText: data?['code'] == -101 ? '去登录' : null,\n                      fn: () {\n                        if (data?['code'] == -101) {\n                          RoutePush.loginRedirectPush();\n                        } else {\n                          setState(() {\n                            _futureBuilderFuture =\n                                _dynamicsController.queryFollowDynamic();\n                            _futureBuilderFutureUp =\n                                _dynamicsController.queryFollowUp();\n                          });\n                        }\n                      },\n                    );\n                  }\n                } else {\n                  // 骨架屏\n                  return skeleton();\n                }\n              },\n            ),\n            const SliverToBoxAdapter(child: SizedBox(height: 40))\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget skeleton() {\n    return SliverList(\n      delegate: SliverChildBuilderDelegate((context, index) {\n        return const DynamicCardSkeleton();\n      }, childCount: 5),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/action_panel.dart",
    "content": "// 操作栏\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/dynamics.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\nimport 'package:pilipala/pages/dynamics/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:status_bar_control/status_bar_control.dart';\nimport 'rich_node_panel.dart';\n\nclass ActionPanel extends StatefulWidget {\n  const ActionPanel({\n    super.key,\n    required this.item,\n  });\n  // ignore: prefer_typing_uninitialized_variables\n  final DynamicItemModel item;\n\n  @override\n  State<ActionPanel> createState() => _ActionPanelState();\n}\n\nclass _ActionPanelState extends State<ActionPanel>\n    with TickerProviderStateMixin {\n  final DynamicsController _dynamicsController = Get.put(DynamicsController());\n  late ModuleStatModel stat;\n  bool isProcessing = false;\n  double defaultHeight = 260;\n  RxDouble height = 0.0.obs;\n  RxBool isExpand = false.obs;\n  late double statusHeight;\n  TextEditingController _inputController = TextEditingController();\n  FocusNode myFocusNode = FocusNode();\n  String _inputText = '';\n\n  void Function()? handleState(Future Function() action) {\n    return isProcessing\n        ? null\n        : () async {\n            isProcessing = true;\n            await action();\n            isProcessing = false;\n          };\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    stat = widget.item.modules!.moduleStat!;\n    onInit();\n  }\n\n  onInit() async {\n    statusHeight = await StatusBarControl.getHeight;\n  }\n\n  // 动态点赞\n  Future onLikeDynamic() async {\n    feedBack();\n    var item = widget.item!;\n    String dynamicId = item.idStr!;\n    // 1 已点赞 2 不喜欢 0 未操作\n    Like like = item.modules!.moduleStat!.like!;\n    int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0');\n    bool status = like.status!;\n    int up = status ? 2 : 1;\n    var res = await DynamicsHttp.likeDynamic(dynamicId: dynamicId, up: up);\n    if (res['status']) {\n      SmartDialog.showToast(!status ? '点赞成功' : '取消赞');\n      if (up == 1) {\n        item.modules!.moduleStat!.like!.count = (count + 1).toString();\n        item.modules!.moduleStat!.like!.status = true;\n      } else {\n        if (count == 1) {\n          item.modules!.moduleStat!.like!.count = '点赞';\n        } else {\n          item.modules!.moduleStat!.like!.count = (count - 1).toString();\n        }\n        item.modules!.moduleStat!.like!.status = false;\n      }\n      setState(() {});\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  // 转发动态预览\n  Widget dynamicPreview() {\n    ItemModulesModel? modules = widget.item.modules;\n    final String type = widget.item.type!;\n    String? cover = modules?.moduleAuthor?.face;\n    switch (type) {\n      /// 图文动态\n      case 'DYNAMIC_TYPE_DRAW':\n        cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url;\n\n      /// 投稿\n      case 'DYNAMIC_TYPE_AV':\n        cover = modules?.moduleDynamic?.major?.archive?.cover;\n\n      /// 转发的动态\n      case 'DYNAMIC_TYPE_FORWARD':\n        String forwardType = widget.item.orig!.type!;\n        switch (forwardType) {\n          /// 图文动态\n          case 'DYNAMIC_TYPE_DRAW':\n            cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url;\n\n          /// 投稿\n          case 'DYNAMIC_TYPE_AV':\n            cover = modules?.moduleDynamic?.major?.archive?.cover;\n\n          /// 专栏文章\n          case 'DYNAMIC_TYPE_ARTICLE':\n            cover = '';\n\n          /// 番剧\n          case 'DYNAMIC_TYPE_PGC':\n            cover = '';\n\n          /// 纯文字动态\n          case 'DYNAMIC_TYPE_WORD':\n            cover = '';\n\n          /// 直播\n          case 'DYNAMIC_TYPE_LIVE_RCMD':\n            cover = '';\n\n          /// 合集查看\n          case 'DYNAMIC_TYPE_UGC_SEASON':\n            cover = '';\n\n          /// 番剧\n          case 'DYNAMIC_TYPE_PGC_UNION':\n            cover = modules?.moduleDynamic?.major?.pgc?.cover;\n\n          default:\n            cover = '';\n        }\n\n      /// 专栏文章\n      case 'DYNAMIC_TYPE_ARTICLE':\n        cover = '';\n\n      /// 番剧\n      case 'DYNAMIC_TYPE_PGC':\n        cover = '';\n\n      /// 纯文字动态\n      case 'DYNAMIC_TYPE_WORD':\n        cover = '';\n\n      /// 直播\n      case 'DYNAMIC_TYPE_LIVE_RCMD':\n        cover = '';\n\n      /// 合集查看\n      case 'DYNAMIC_TYPE_UGC_SEASON':\n        cover = '';\n\n      /// 番剧查看\n      case 'DYNAMIC_TYPE_PGC_UNION':\n        cover = '';\n\n      default:\n        cover = '';\n    }\n    return Container(\n      width: double.infinity,\n      height: 95,\n      margin: const EdgeInsets.fromLTRB(12, 0, 12, 14),\n      decoration: BoxDecoration(\n        color:\n            Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4),\n        borderRadius: BorderRadius.circular(6),\n        border: Border(\n          left: BorderSide(\n              width: 4,\n              color: Theme.of(context).colorScheme.primary.withOpacity(0.8)),\n        ),\n      ),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              '@${widget.item.modules!.moduleAuthor!.name}',\n              style: TextStyle(\n                color: Theme.of(context).colorScheme.primary,\n              ),\n            ),\n            const SizedBox(height: 8),\n            Row(\n              children: [\n                NetworkImgLayer(\n                  src: cover ?? '',\n                  width: 34,\n                  height: 34,\n                  type: 'emote',\n                ),\n                const SizedBox(width: 10),\n                Expanded(\n                  child: Text.rich(\n                    style: const TextStyle(height: 0),\n                    richNode(widget.item, context),\n                    maxLines: 2,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n                // Text(data)\n              ],\n            )\n          ],\n        ),\n      ),\n    );\n  }\n\n  // 动态转发\n  void forwardHandler() async {\n    showModalBottomSheet(\n      context: context,\n      enableDrag: false,\n      useRootNavigator: true,\n      isScrollControlled: true,\n      builder: (context) {\n        return Obx(\n          () => AnimatedContainer(\n            duration: Durations.medium1,\n            onEnd: () async {\n              if (isExpand.value) {\n                await Future.delayed(const Duration(milliseconds: 80));\n                myFocusNode.requestFocus();\n              }\n            },\n            height: height.value + MediaQuery.of(context).padding.bottom,\n            child: Column(\n              children: [\n                AnimatedContainer(\n                  duration: Durations.medium1,\n                  height: isExpand.value ? statusHeight : 0,\n                ),\n                Padding(\n                  padding: EdgeInsets.fromLTRB(\n                    isExpand.value ? 10 : 16,\n                    10,\n                    isExpand.value ? 14 : 12,\n                    0,\n                  ),\n                  child: Row(\n                    mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                    children: [\n                      if (isExpand.value) ...[\n                        IconButton(\n                          onPressed: () => togglePanelState(false),\n                          icon: const Icon(Icons.close),\n                        ),\n                        Text(\n                          '转发动态',\n                          style: Theme.of(context)\n                              .textTheme\n                              .titleMedium!\n                              .copyWith(fontWeight: FontWeight.bold),\n                        )\n                      ] else ...[\n                        const Text(\n                          '转发动态',\n                          style: TextStyle(fontWeight: FontWeight.bold),\n                        )\n                      ],\n                      isExpand.value\n                          ? FilledButton(\n                              onPressed: () => dynamicForward('forward'),\n                              child: const Text('转发'),\n                            )\n                          : TextButton(\n                              onPressed: () {},\n                              child: const Text('立即转发'),\n                            )\n                    ],\n                  ),\n                ),\n                if (!isExpand.value) ...[\n                  GestureDetector(\n                    onTap: () => togglePanelState(true),\n                    behavior: HitTestBehavior.translucent,\n                    child: Container(\n                      width: double.infinity,\n                      alignment: Alignment.centerLeft,\n                      padding: const EdgeInsets.fromLTRB(16, 0, 10, 14),\n                      child: Text(\n                        '说点什么吧',\n                        textAlign: TextAlign.start,\n                        style: TextStyle(\n                            color: Theme.of(context).colorScheme.outline),\n                      ),\n                    ),\n                  ),\n                ] else ...[\n                  Padding(\n                    padding: const EdgeInsets.fromLTRB(16, 10, 16, 0),\n                    child: TextField(\n                      maxLines: 5,\n                      focusNode: myFocusNode,\n                      controller: _inputController,\n                      onChanged: (value) {\n                        setState(() {\n                          _inputText = value;\n                        });\n                      },\n                      decoration: const InputDecoration(\n                        border: InputBorder.none,\n                        hintText: '说点什么吧',\n                      ),\n                    ),\n                  ),\n                ],\n                dynamicPreview(),\n                if (!isExpand.value) ...[\n                  const Divider(thickness: 0.1, height: 1),\n                  ListTile(\n                    onTap: () => Get.back(),\n                    minLeadingWidth: 0,\n                    dense: true,\n                    title: Text(\n                      '取消',\n                      style: TextStyle(\n                          color: Theme.of(context).colorScheme.outline),\n                      textAlign: TextAlign.center,\n                    ),\n                  ),\n                ]\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  togglePanelState(status) {\n    if (!status) {\n      Get.back();\n      height.value = defaultHeight;\n      _inputText = '';\n      _inputController.clear();\n    } else {\n      height.value = Get.size.height;\n    }\n    isExpand.value = !(isExpand.value);\n  }\n\n  dynamicForward(String type) async {\n    String dynamicId = widget.item.idStr!;\n    var res = await DynamicsHttp.dynamicCreate(\n      dynIdStr: dynamicId,\n      mid: _dynamicsController.userInfo.mid,\n      rawText: _inputText,\n      scene: 4,\n    );\n    if (res['status']) {\n      SmartDialog.showToast(type == 'forward' ? '转发成功' : '发布成功');\n      togglePanelState(false);\n    }\n  }\n\n  @override\n  void dispose() {\n    myFocusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var color = Theme.of(context).colorScheme.outline;\n    var primary = Theme.of(context).colorScheme.primary;\n    height.value = defaultHeight;\n    print('height.value: ${height.value}');\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.spaceAround,\n      children: [\n        Expanded(\n          flex: 1,\n          child: TextButton.icon(\n            onPressed: forwardHandler,\n            icon: const Icon(\n              FontAwesomeIcons.shareFromSquare,\n              size: 16,\n            ),\n            style: TextButton.styleFrom(\n              padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),\n              foregroundColor: Theme.of(context).colorScheme.outline,\n            ),\n            label: Text(stat.forward!.count ?? '转发'),\n          ),\n        ),\n        Expanded(\n          flex: 1,\n          child: TextButton.icon(\n            onPressed: () => _dynamicsController.pushDetail(widget.item, 1,\n                action: 'comment'),\n            icon: const Icon(\n              FontAwesomeIcons.comment,\n              size: 16,\n            ),\n            style: TextButton.styleFrom(\n              padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),\n              foregroundColor: Theme.of(context).colorScheme.outline,\n            ),\n            label: Text(stat.comment!.count ?? '评论'),\n          ),\n        ),\n        Expanded(\n          flex: 1,\n          child: TextButton.icon(\n            onPressed: handleState(onLikeDynamic),\n            icon: Icon(\n              stat.like!.status!\n                  ? FontAwesomeIcons.solidThumbsUp\n                  : FontAwesomeIcons.thumbsUp,\n              size: 16,\n              color: stat.like!.status! ? primary : color,\n            ),\n            style: TextButton.styleFrom(\n              padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),\n              foregroundColor: Theme.of(context).colorScheme.outline,\n            ),\n            label: AnimatedSwitcher(\n              duration: const Duration(milliseconds: 400),\n              transitionBuilder: (Widget child, Animation<double> animation) {\n                return ScaleTransition(scale: animation, child: child);\n              },\n              child: Text(\n                stat.like!.count ?? '点赞',\n                key: ValueKey<String>(stat.like!.count ?? '点赞'),\n                style: TextStyle(\n                  color: stat.like!.status! ? primary : color,\n                ),\n              ),\n            ),\n          ),\n        )\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/additional_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/search.dart';\n\n/// TODO 点击跳转\nWidget addWidget(item, context, type, {floor = 1}) {\n  Map<dynamic, dynamic> dynamicProperty = {\n    'ADDITIONAL_TYPE_UGC': item.modules.moduleDynamic.additional.ugc,\n    // 直播预约\n    'ADDITIONAL_TYPE_RESERVE': item.modules.moduleDynamic.additional.reserve,\n    // 商品\n    'ADDITIONAL_TYPE_GOODS': item.modules.moduleDynamic.additional.goods,\n    // 比赛信息\n    'ADDITIONAL_TYPE_MATCH': item.modules.moduleDynamic.additional.match,\n    // 游戏信息\n    'ADDITIONAL_TYPE_COMMON': item.modules.moduleDynamic.additional.common,\n  };\n  Color bgColor = floor == 1\n      ? Theme.of(context).dividerColor.withOpacity(0.08)\n      : Theme.of(context).colorScheme.surface;\n  switch (type) {\n    case 'ADDITIONAL_TYPE_UGC':\n      // 转发的投稿\n      return InkWell(\n        onTap: () async {\n          String text = dynamicProperty[type].jumpUrl;\n          RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);\n          Iterable<Match> matches = bvRegex.allMatches(text);\n          if (matches.isNotEmpty) {\n            Match match = matches.first;\n            String bvid = match.group(0)!;\n            String cover = dynamicProperty[type].cover;\n            try {\n              int cid = await SearchHttp.ab2c(bvid: bvid);\n              Get.toNamed('/video?bvid=$bvid&cid=$cid',\n                  arguments: {'pic': cover, 'heroTag': bvid});\n            } catch (err) {\n              SmartDialog.showToast(err.toString());\n            }\n          } else {\n            print(\"No match found.\");\n          }\n        },\n        child: Container(\n          padding:\n              const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),\n          color: bgColor,\n          child: Row(\n            children: [\n              NetworkImgLayer(\n                width: 120,\n                height: 75,\n                src: dynamicProperty[type].cover,\n              ),\n              const SizedBox(width: 10),\n              Expanded(\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  mainAxisAlignment: MainAxisAlignment.start,\n                  children: [\n                    Text(\n                      dynamicProperty[type].title,\n                      maxLines: 2,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                    const SizedBox(height: 4),\n                    Text(\n                      dynamicProperty[type].descSecond,\n                      style: TextStyle(\n                        color: Theme.of(context).colorScheme.outline,\n                        fontSize:\n                            Theme.of(context).textTheme.labelMedium!.fontSize,\n                      ),\n                    )\n                  ],\n                ),\n              ),\n            ],\n          ),\n        ),\n      );\n    case 'ADDITIONAL_TYPE_RESERVE':\n      return dynamicProperty[type].state != -1\n          ? dynamicProperty[type].title != null\n              ? Padding(\n                  padding: const EdgeInsets.only(top: 8),\n                  child: InkWell(\n                    onTap: () {},\n                    child: Container(\n                      width: double.infinity,\n                      padding: const EdgeInsets.only(\n                          left: 12, top: 10, right: 12, bottom: 10),\n                      color: bgColor,\n                      child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          Text(\n                            dynamicProperty[type].title,\n                            maxLines: 1,\n                            overflow: TextOverflow.ellipsis,\n                          ),\n                          const SizedBox(height: 1),\n                          Text.rich(\n                            TextSpan(\n                              style: TextStyle(\n                                  color: Theme.of(context).colorScheme.outline,\n                                  fontSize: Theme.of(context)\n                                      .textTheme\n                                      .labelMedium!\n                                      .fontSize),\n                              children: [\n                                if (dynamicProperty[type].desc1 != null)\n                                  TextSpan(\n                                      text:\n                                          dynamicProperty[type].desc1['text']),\n                                const TextSpan(text: '  '),\n                                if (dynamicProperty[type].desc2 != null)\n                                  TextSpan(\n                                      text:\n                                          dynamicProperty[type].desc2['text']),\n                              ],\n                            ),\n                          )\n                        ],\n                      ),\n                      // TextButton(onPressed: () {}, child: Text('123'))\n                    ),\n                  ),\n                )\n              : const SizedBox()\n          : const SizedBox();\n    case 'ADDITIONAL_TYPE_GOODS':\n      // 商品\n      return const SizedBox();\n    // return Padding(\n    //     padding: const EdgeInsets.only(top: 6),\n    //     child: InkWell(\n    //       onTap: () {},\n    //       child: Container(\n    //         padding:\n    //             const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),\n    //         decoration: BoxDecoration(\n    //           color: bgColor,\n    //           borderRadius: const BorderRadius.all(Radius.circular(6)),\n    //         ),\n    //         child: Row(\n    //           children: [\n    //             NetworkImgLayer(\n    //               width: 75,\n    //               height: 75,\n    //               src: dynamicProperty[type].items.first.cover,\n    //             ),\n    //             const SizedBox(width: 10),\n    //             Expanded(\n    //               child: Column(\n    //                 crossAxisAlignment: CrossAxisAlignment.start,\n    //                 mainAxisAlignment: MainAxisAlignment.start,\n    //                 children: [\n    //                   Text(\n    //                     dynamicProperty[type].items.first.name,\n    //                     maxLines: 1,\n    //                     overflow: TextOverflow.ellipsis,\n    //                   ),\n    //                   Text(\n    //                     dynamicProperty[type].items.first.brief,\n    //                     maxLines: 1,\n    //                     style: TextStyle(\n    //                       color: Theme.of(context).colorScheme.outline,\n    //                       fontSize: Theme.of(context)\n    //                           .textTheme\n    //                           .labelMedium!\n    //                           .fontSize,\n    //                     ),\n    //                   ),\n    //                   const SizedBox(height: 2),\n    //                   Text(\n    //                     dynamicProperty[type].items.first.price,\n    //                     style: TextStyle(\n    //                       color: Theme.of(context).colorScheme.primary,\n    //                     ),\n    //                   ),\n    //                 ],\n    //               ),\n    //             ),\n    //           ],\n    //         ),\n    //       ),\n    //     ),);\n    case 'ADDITIONAL_TYPE_MATCH':\n      return const SizedBox();\n    case 'ADDITIONAL_TYPE_COMMON':\n      return const SizedBox();\n    case 'ADDITIONAL_TYPE_VOTE':\n      return const SizedBox();\n    default:\n      return const Text('11');\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/article_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'pic_panel.dart';\n\nWidget articlePanel(item, context, {floor = 1}) {\n  TextStyle authorStyle =\n      TextStyle(color: Theme.of(context).colorScheme.primary);\n  return Padding(\n    padding: floor == 2\n        ? EdgeInsets.zero\n        : const EdgeInsets.only(left: 12, right: 12),\n    child: Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        if (floor == 2) ...[\n          Row(\n            children: [\n              GestureDetector(\n                onTap: () {},\n                child: Text(\n                  '@${item.modules.moduleAuthor.name}',\n                  style: authorStyle,\n                ),\n              ),\n              const SizedBox(width: 6),\n              Text(\n                Utils.dateFormat(item.modules.moduleAuthor.pubTs),\n                style: TextStyle(\n                    color: Theme.of(context).colorScheme.outline,\n                    fontSize: Theme.of(context).textTheme.labelSmall!.fontSize),\n              ),\n            ],\n          ),\n          const SizedBox(height: 8),\n        ],\n        // Text(\n        //   item.modules.moduleDynamic.major.opus.title,\n        //   style: Theme.of(context)\n        //       .textTheme\n        //       .titleMedium!\n        //       .copyWith(fontWeight: FontWeight.bold),\n        // ),\n        // const SizedBox(height: 2),\n        // if (item.modules.moduleDynamic.major.opus.summary.text !=\n        //     'undefined') ...[\n        //   Text(\n        //     item.modules.moduleDynamic.major.opus.summary.richTextNodes.first\n        //         .text,\n        //     maxLines: 4,\n        //     style: const TextStyle(height: 1.55),\n        //     overflow: TextOverflow.ellipsis,\n        //   ),\n        //   const SizedBox(height: 2),\n        // ],\n        picWidget(item, context)\n      ],\n    ),\n  );\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/author_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass AuthorPanel extends StatelessWidget {\n  final dynamic item;\n  const AuthorPanel({super.key, required this.item});\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);\n    return Row(\n      children: [\n        GestureDetector(\n          onTap: () {\n            // 番剧\n            if (item.modules.moduleAuthor.type == 'AUTHOR_TYPE_PGC') {\n              return;\n            }\n            feedBack();\n            Get.toNamed(\n              '/member?mid=${item.modules.moduleAuthor.mid}',\n              arguments: {\n                'face': item.modules.moduleAuthor.face,\n                'heroTag': heroTag\n              },\n            );\n          },\n          child: Hero(\n            tag: heroTag,\n            child: NetworkImgLayer(\n              width: 40,\n              height: 40,\n              type: 'avatar',\n              src: item.modules.moduleAuthor.face,\n            ),\n          ),\n        ),\n        const SizedBox(width: 10),\n        Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              children: [\n                Text(\n                  item.modules.moduleAuthor.name,\n                  style: TextStyle(\n                    color: item.modules.moduleAuthor!.vip != null &&\n                            item.modules.moduleAuthor!.vip['status'] > 0\n                        ? const Color.fromARGB(255, 251, 100, 163)\n                        : Theme.of(context).colorScheme.onSurface,\n                    fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,\n                  ),\n                ),\n              ],\n            ),\n            DefaultTextStyle.merge(\n              style: TextStyle(\n                color: Theme.of(context).colorScheme.outline,\n                fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,\n              ),\n              child: Row(\n                children: [\n                  Text(item.modules.moduleAuthor.pubTime),\n                  if (item.modules.moduleAuthor.pubTime != '' &&\n                      item.modules.moduleAuthor.pubAction != '')\n                    const Text(' '),\n                  Text(item.modules.moduleAuthor.pubAction),\n                ],\n              ),\n            )\n          ],\n        ),\n        const Spacer(),\n        if (item.type == 'DYNAMIC_TYPE_AV')\n          SizedBox(\n            width: 32,\n            height: 32,\n            child: IconButton(\n              style: ButtonStyle(\n                padding: MaterialStateProperty.all(EdgeInsets.zero),\n              ),\n              onPressed: () {\n                showModalBottomSheet(\n                  context: context,\n                  useRootNavigator: true,\n                  isScrollControlled: true,\n                  builder: (context) {\n                    return MorePanel(item: item);\n                  },\n                );\n              },\n              icon: const Icon(Icons.more_vert_outlined, size: 18),\n            ),\n          ),\n      ],\n    );\n  }\n}\n\nclass MorePanel extends StatelessWidget {\n  final dynamic item;\n  const MorePanel({super.key, required this.item});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),\n      // clipBehavior: Clip.hardEdge,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          InkWell(\n            onTap: () => Get.back(),\n            child: Container(\n              height: 35,\n              padding: const EdgeInsets.only(bottom: 2),\n              child: Center(\n                child: Container(\n                  width: 32,\n                  height: 3,\n                  decoration: BoxDecoration(\n                      color: Theme.of(context).colorScheme.outline,\n                      borderRadius: const BorderRadius.all(Radius.circular(3))),\n                ),\n              ),\n            ),\n          ),\n          ListTile(\n            onTap: () async {\n              try {\n                String bvid = item.modules.moduleDynamic.major.archive.bvid;\n                var res = await UserHttp.toViewLater(bvid: bvid);\n                SmartDialog.showToast(res['msg']);\n                Get.back();\n              } catch (err) {\n                SmartDialog.showToast('出错了：${err.toString()}');\n              }\n            },\n            minLeadingWidth: 0,\n            // dense: true,\n            leading: const Icon(Icons.watch_later_outlined, size: 19),\n            title: Text(\n              '稍后再看',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ),\n          const Divider(thickness: 0.1, height: 1),\n          ListTile(\n            onTap: () => Get.back(),\n            minLeadingWidth: 0,\n            dense: true,\n            title: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              textAlign: TextAlign.center,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/content_panel.dart",
    "content": "// 内容\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\nimport 'package:pilipala/plugin/pl_gallery/index.dart';\nimport 'rich_node_panel.dart';\n\n// ignore: must_be_immutable\nclass Content extends StatefulWidget {\n  dynamic item;\n  String? source;\n  Content({\n    super.key,\n    this.item,\n    this.source,\n  });\n\n  @override\n  State<Content> createState() => _ContentState();\n}\n\nclass _ContentState extends State<Content> {\n  late bool hasPics;\n  List<OpusPicsModel> pics = [];\n\n  @override\n  void initState() {\n    super.initState();\n    hasPics = widget.item.modules.moduleDynamic.major != null &&\n        widget.item.modules.moduleDynamic.major.opus != null &&\n        widget.item.modules.moduleDynamic.major.opus.pics.isNotEmpty;\n    if (hasPics) {\n      pics = widget.item.modules.moduleDynamic.major.opus.pics;\n    }\n  }\n\n  InlineSpan picsNodes() {\n    List<InlineSpan> spanChilds = [];\n    int len = pics.length;\n    List<String> picList = [];\n\n    if (len == 1) {\n      OpusPicsModel pictureItem = pics.first;\n      picList.add(pictureItem.url!);\n\n      /// 图片上方的空白间隔\n      // spanChilds.add(const TextSpan(text: '\\n'));\n      spanChilds.add(\n        WidgetSpan(\n          child: LayoutBuilder(\n            builder: (context, BoxConstraints box) {\n              double maxWidth = box.maxWidth.truncateToDouble();\n              double maxHeight = box.maxWidth * 0.6; // 设置最大高度\n              double height = maxWidth *\n                  0.5 *\n                  (pictureItem.height != null && pictureItem.width != null\n                      ? pictureItem.height! / pictureItem.width!\n                      : 1);\n              return Hero(\n                tag: pictureItem.url!,\n                placeholderBuilder:\n                    (BuildContext context, Size heroSize, Widget child) {\n                  return child;\n                },\n                child: GestureDetector(\n                  onTap: () => onPreviewImg(picList, 1, context),\n                  child: Container(\n                    padding: const EdgeInsets.only(top: 4),\n                    constraints: BoxConstraints(maxHeight: maxHeight),\n                    width: box.maxWidth / 2,\n                    height: height,\n                    child: Stack(\n                      children: [\n                        Positioned.fill(\n                          child: NetworkImgLayer(\n                            src: pictureItem.url,\n                            width: maxWidth / 2,\n                            height: height,\n                          ),\n                        ),\n                        height > Get.size.height * 0.9\n                            ? const PBadge(\n                                text: '长图',\n                                right: 8,\n                                bottom: 8,\n                              )\n                            : const SizedBox(),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      );\n    }\n    if (len > 1) {\n      List<Widget> list = [];\n      for (var i = 0; i < len; i++) {\n        picList.add(pics[i].url!);\n      }\n      for (var i = 0; i < len; i++) {\n        list.add(\n          LayoutBuilder(\n            builder: (context, BoxConstraints box) {\n              double maxWidth = box.maxWidth.truncateToDouble();\n              return Hero(\n                tag: picList[i],\n                child: GestureDetector(\n                  onTap: () => onPreviewImg(picList, i, context),\n                  child: NetworkImgLayer(\n                    src: pics[i].url,\n                    width: maxWidth,\n                    height: maxWidth,\n                    origAspectRatio:\n                        pics[i].width!.toInt() / pics[i].height!.toInt(),\n                  ),\n                ),\n              );\n            },\n          ),\n        );\n      }\n      spanChilds.add(\n        WidgetSpan(\n          child: LayoutBuilder(\n            builder: (context, BoxConstraints box) {\n              double maxWidth = box.maxWidth.truncateToDouble();\n              double crossCount = len < 3 ? 2 : 3;\n              double height = maxWidth /\n                      crossCount *\n                      (len % crossCount == 0\n                          ? len ~/ crossCount\n                          : len ~/ crossCount + 1) +\n                  6;\n              return Container(\n                padding: const EdgeInsets.only(top: 6),\n                height: height,\n                child: GridView.count(\n                  padding: EdgeInsets.zero,\n                  physics: const NeverScrollableScrollPhysics(),\n                  crossAxisCount: crossCount.toInt(),\n                  mainAxisSpacing: 4.0,\n                  crossAxisSpacing: 4.0,\n                  childAspectRatio: 1,\n                  children: list,\n                ),\n              );\n            },\n          ),\n        ),\n      );\n    }\n    return TextSpan(\n      children: spanChilds,\n    );\n  }\n\n  void onPreviewImg(picList, initIndex, context) {\n    Navigator.of(context).push(\n      HeroDialogRoute<void>(\n        builder: (BuildContext context) => InteractiveviewerGallery(\n          sources: picList,\n          initIndex: initIndex,\n          onPageChanged: (int pageIndex) {},\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle authorStyle =\n        TextStyle(color: Theme.of(context).colorScheme.primary);\n\n    return Container(\n      width: double.infinity,\n      padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          if (widget.item.modules.moduleDynamic.topic != null) ...[\n            GestureDetector(\n              child: Text(\n                '#${widget.item.modules.moduleDynamic.topic.name}',\n                style: authorStyle,\n              ),\n            ),\n          ],\n          IgnorePointer(\n            // 禁用SelectableRegion的触摸交互功能\n            ignoring: widget.source == 'detail' ? false : true,\n            child: SelectableRegion(\n              magnifierConfiguration: const TextMagnifierConfiguration(),\n              focusNode: FocusNode(),\n              selectionControls: MaterialTextSelectionControls(),\n              child: Text.rich(\n                /// fix 默认20px高度\n                style: const TextStyle(height: 0),\n                richNode(widget.item, context),\n                maxLines: widget.source == 'detail' ? 999 : 3,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n          ),\n          if (hasPics) ...[\n            Text.rich(picsNodes()),\n          ]\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/dynamic_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/pages/dynamics/index.dart';\nimport 'action_panel.dart';\nimport 'author_panel.dart';\nimport 'content_panel.dart';\nimport 'forward_panel.dart';\n\nclass DynamicPanel extends StatelessWidget {\n  final dynamic item;\n  final String? source;\n  DynamicPanel({required this.item, this.source, Key? key}) : super(key: key);\n  final DynamicsController _dynamicsController = Get.put(DynamicsController());\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: source == 'detail'\n          ? const EdgeInsets.only(bottom: 12)\n          : EdgeInsets.zero,\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            width: 8,\n            color: Theme.of(context).dividerColor.withOpacity(0.05),\n          ),\n        ),\n      ),\n      child: Material(\n        elevation: 0,\n        clipBehavior: Clip.hardEdge,\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(0),\n        ),\n        child: InkWell(\n          onTap: () => _dynamicsController.pushDetail(item, 1),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.start,\n            children: [\n              Padding(\n                padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),\n                child: AuthorPanel(item: item),\n              ),\n              if (item.modules!.moduleDynamic!.desc != null ||\n                  item.modules!.moduleDynamic!.major != null)\n                Content(item: item, source: source),\n              forWard(item, context, _dynamicsController, source),\n              const SizedBox(height: 2),\n              if (source == null) ActionPanel(item: item),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/forward_panel.dart",
    "content": "// 转发\nimport 'package:flutter/material.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'additional_panel.dart';\nimport 'article_panel.dart';\nimport 'live_panel.dart';\nimport 'live_rcmd_panel.dart';\nimport 'pic_panel.dart';\nimport 'rich_node_panel.dart';\nimport 'video_panel.dart';\n\nWidget forWard(item, context, ctr, source, {floor = 1}) {\n  TextStyle authorStyle =\n      TextStyle(color: Theme.of(context).colorScheme.primary);\n  switch (item.type) {\n    // 图文\n    case 'DYNAMIC_TYPE_DRAW':\n      return Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          if (floor == 2) ...[\n            Row(\n              children: [\n                GestureDetector(\n                  onTap: () => Get.toNamed(\n                      '/member?mid=${item.modules.moduleAuthor.mid}',\n                      arguments: {'face': item.modules.moduleAuthor.face}),\n                  child: Text(\n                    '@${item.modules.moduleAuthor.name}',\n                    style: authorStyle,\n                  ),\n                ),\n                const SizedBox(width: 6),\n                Text(\n                  Utils.dateFormat(item.modules.moduleAuthor.pubTs),\n                  style: TextStyle(\n                      color: Theme.of(context).colorScheme.outline,\n                      fontSize:\n                          Theme.of(context).textTheme.labelSmall!.fontSize),\n                ),\n              ],\n            ),\n            const SizedBox(height: 2),\n\n            /// fix #话题跟content重复\n            // if (item.modules.moduleDynamic.topic != null) ...[\n            //   Padding(\n            //     padding: floor == 2\n            //         ? EdgeInsets.zero\n            //         : const EdgeInsets.only(left: 12, right: 12),\n            //     child: GestureDetector(\n            //       child: Text(\n            //         '#${item.modules.moduleDynamic.topic.name}',\n            //         style: authorStyle,\n            //       ),\n            //     ),\n            //   ),\n            // ],\n            Text.rich(\n              richNode(item, context),\n              // 被转发状态(floor=2) 隐藏\n              maxLines: source == 'detail' && floor != 2 ? 999 : 4,\n              overflow: TextOverflow.ellipsis,\n            ),\n            const SizedBox(height: 4),\n          ],\n          Padding(\n            padding: floor == 2\n                ? EdgeInsets.zero\n                : const EdgeInsets.only(left: 12, right: 12),\n            child: picWidget(item, context),\n          ),\n\n          /// 附加内容 商品信息、直播预约等等\n          if (item.modules.moduleDynamic.additional != null)\n            addWidget(\n              item,\n              context,\n              item.modules.moduleDynamic.additional.type,\n              floor: floor,\n            )\n        ],\n      );\n    // 视频\n    case 'DYNAMIC_TYPE_AV':\n      return videoSeasonWidget(item, context, 'archive', floor: floor);\n    // 文章\n    case 'DYNAMIC_TYPE_ARTICLE':\n      return articlePanel(item, context, floor: floor);\n    // 转发\n    case 'DYNAMIC_TYPE_FORWARD':\n      return InkWell(\n        onTap: () => ctr.pushDetail(item.orig, floor + 1),\n        child: Container(\n          padding:\n              const EdgeInsets.only(left: 15, top: 10, right: 15, bottom: 8),\n          color: Theme.of(context).dividerColor.withOpacity(0.08),\n          child: forWard(item.orig, context, ctr, source, floor: floor + 1),\n        ),\n      );\n    // 直播\n    case 'DYNAMIC_TYPE_LIVE_RCMD':\n      return liveRcmdPanel(item, context, floor: floor);\n    // 直播\n    case 'DYNAMIC_TYPE_LIVE':\n      return livePanel(item, context, floor: floor);\n    // 合集\n    case 'DYNAMIC_TYPE_UGC_SEASON':\n      return videoSeasonWidget(item, context, 'ugcSeason');\n    case 'DYNAMIC_TYPE_WORD':\n      return floor == 2\n          ? Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Row(\n                  children: [\n                    GestureDetector(\n                      onTap: () => Get.toNamed(\n                          '/member?mid=${item.modules.moduleAuthor.mid}',\n                          arguments: {'face': item.modules.moduleAuthor.face}),\n                      child: Text(\n                        '@${item.modules.moduleAuthor.name}',\n                        style: authorStyle,\n                      ),\n                    ),\n                    const SizedBox(width: 6),\n                    Text(\n                      Utils.dateFormat(item.modules.moduleAuthor.pubTs),\n                      style: TextStyle(\n                          color: Theme.of(context).colorScheme.outline,\n                          fontSize:\n                              Theme.of(context).textTheme.labelSmall!.fontSize),\n                    ),\n                  ],\n                ),\n                const SizedBox(height: 8),\n                Text.rich(\n                  richNode(item, context),\n                  // 被转发状态(floor=2) 隐藏\n                  maxLines: source == 'detail' && floor != 2 ? 999 : 4,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ],\n            )\n          : item.modules.moduleDynamic.additional != null\n              ? addWidget(\n                  item,\n                  context,\n                  item.modules.moduleDynamic.additional.type,\n                  floor: floor,\n                )\n              : const SizedBox(height: 0);\n    case 'DYNAMIC_TYPE_PGC':\n      return videoSeasonWidget(item, context, 'pgc', floor: floor);\n    case 'DYNAMIC_TYPE_PGC_UNION':\n      return videoSeasonWidget(item, context, 'pgc', floor: floor);\n    // 直播结束\n    case 'DYNAMIC_TYPE_NONE':\n      return Row(\n        children: [\n          const Icon(\n            FontAwesomeIcons.ghost,\n            size: 14,\n          ),\n          const SizedBox(width: 4),\n          Text(item.modules.moduleDynamic.major.none.tips)\n        ],\n      );\n    // 课堂\n    case 'DYNAMIC_TYPE_COURSES_SEASON':\n      return Row(\n        children: [\n          Expanded(\n            child: Text(\n              \"课堂💪：${item.modules.moduleDynamic.major.courses['title']}\",\n              maxLines: 1,\n              overflow: TextOverflow.ellipsis,\n            ),\n          )\n        ],\n      );\n    // 活动\n    case 'DYNAMIC_TYPE_COMMON_SQUARE':\n      return Padding(\n        padding: const EdgeInsets.only(top: 8),\n        child: InkWell(\n          onTap: () {\n            Get.toNamed('/webview', parameters: {\n              'url': item.modules.moduleDynamic.major.common['jump_url'],\n              'type': 'url',\n              'pageTitle': item.modules.moduleDynamic.major.common['title']\n            });\n          },\n          child: Container(\n            width: double.infinity,\n            padding:\n                const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10),\n            color: Theme.of(context).dividerColor.withOpacity(0.08),\n            child: Row(\n              children: [\n                NetworkImgLayer(\n                  width: 45,\n                  height: 45,\n                  src: item.modules.moduleDynamic.major.common['cover'],\n                ),\n                const SizedBox(width: 10),\n                Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      item.modules.moduleDynamic.major.common['title'],\n                      style: TextStyle(\n                        color: Theme.of(context).colorScheme.primary,\n                      ),\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                    const SizedBox(height: 2),\n                    Text(\n                      item.modules.moduleDynamic.major.common['desc'],\n                      style: TextStyle(\n                        color: Theme.of(context).colorScheme.outline,\n                        fontSize:\n                            Theme.of(context).textTheme.labelMedium!.fontSize,\n                      ),\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ],\n                )\n              ],\n            ),\n            // TextButton(onPressed: () {}, child: Text('123'))\n          ),\n        ),\n      );\n    case 'DYNAMIC_TYPE_MUSIC':\n      final Map music = item.modules.moduleDynamic.major.music;\n      return Padding(\n        padding: const EdgeInsets.only(top: 8),\n        child: InkWell(\n          onTap: () {\n            Get.toNamed('/webview', parameters: {\n              'url': \"https:${music['jump_url']}\",\n              'type': 'url',\n              'pageTitle': music['title']\n            });\n          },\n          child: Container(\n            width: double.infinity,\n            padding:\n                const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10),\n            color: Theme.of(context).dividerColor.withOpacity(0.08),\n            child: Row(\n              children: [\n                NetworkImgLayer(\n                  width: 45,\n                  height: 45,\n                  src: music['cover'],\n                ),\n                const SizedBox(width: 10),\n                Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      music['title'],\n                      style: TextStyle(\n                        color: Theme.of(context).colorScheme.primary,\n                      ),\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                    const SizedBox(height: 2),\n                    Text(\n                      music['label'],\n                      style: TextStyle(\n                        color: Theme.of(context).colorScheme.outline,\n                        fontSize:\n                            Theme.of(context).textTheme.labelMedium!.fontSize,\n                      ),\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ],\n                )\n              ],\n            ),\n            // TextButton(onPressed: () {}, child: Text('123'))\n          ),\n        ),\n      );\n    default:\n      return const SizedBox(\n        width: double.infinity,\n        child: Text('🙏 暂未支持的类型，请联系开发者反馈 '),\n      );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/live_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'rich_node_panel.dart';\n\nWidget livePanel(item, context, {floor = 1}) {\n  dynamic content = item.modules.moduleDynamic.major;\n  TextStyle authorStyle =\n      TextStyle(color: Theme.of(context).colorScheme.primary);\n  return Column(\n    crossAxisAlignment: CrossAxisAlignment.start,\n    children: [\n      if (floor == 2) ...[\n        Row(\n          children: [\n            GestureDetector(\n              onTap: () => Get.toNamed(\n                  '/member?mid=${item.modules.moduleAuthor.mid}',\n                  arguments: {'face': item.modules.moduleAuthor.face}),\n              child: Text(\n                '@${item.modules.moduleAuthor.name}',\n                style: authorStyle,\n              ),\n            ),\n            const SizedBox(width: 6),\n            Text(\n              Utils.dateFormat(item.modules.moduleAuthor.pubTs),\n              style: TextStyle(\n                  color: Theme.of(context).colorScheme.outline,\n                  fontSize: Theme.of(context).textTheme.labelSmall!.fontSize),\n            ),\n          ],\n        ),\n      ],\n      const SizedBox(height: 4),\n      if (item.modules.moduleDynamic.topic != null) ...[\n        Padding(\n          padding: floor == 2\n              ? EdgeInsets.zero\n              : const EdgeInsets.only(left: 12, right: 12),\n          child: GestureDetector(\n            child: Text(\n              '#${item.modules.moduleDynamic.topic.name}',\n              style: authorStyle,\n            ),\n          ),\n        ),\n        const SizedBox(height: 6),\n      ],\n      if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[\n        Text.rich(richNode(item, context)),\n        const SizedBox(height: 6),\n      ],\n      GestureDetector(\n        onTap: () {},\n        child: Row(\n          mainAxisAlignment: MainAxisAlignment.start,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            NetworkImgLayer(\n              width: 120,\n              height: 75,\n              src: content.live.cover,\n            ),\n            const SizedBox(width: 10),\n            Expanded(\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                mainAxisAlignment: MainAxisAlignment.start,\n                children: [\n                  Text(\n                    content.live.title,\n                    maxLines: 2,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                  const SizedBox(height: 4),\n                  Text(\n                    content.live.descFirst,\n                    style: TextStyle(\n                      color: Theme.of(context).colorScheme.outline,\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                    ),\n                  )\n                ],\n              ),\n            ),\n            Text(\n              content.live.badge['text'],\n              style: TextStyle(\n                fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n              ),\n            )\n          ],\n        ),\n      ),\n    ],\n  );\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/live_rcmd_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\nimport 'package:pilipala/pages/dynamics/index.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'rich_node_panel.dart';\n\nfinal DynamicsController _dynamicsController = Get.put(DynamicsController());\nWidget liveRcmdPanel(item, context, {floor = 1}) {\n  TextStyle authorStyle =\n      TextStyle(color: Theme.of(context).colorScheme.primary);\n  DynamicLiveModel liveRcmd = item.modules.moduleDynamic.major.liveRcmd;\n  int liveStatus = liveRcmd.liveStatus!;\n  Map watchedShow = liveRcmd.watchedShow!;\n  return Column(\n    crossAxisAlignment: CrossAxisAlignment.start,\n    children: [\n      if (floor == 2) ...[\n        Row(\n          children: [\n            GestureDetector(\n              onTap: () => Get.toNamed(\n                  '/member?mid=${item.modules.moduleAuthor.mid}',\n                  arguments: {'face': item.modules.moduleAuthor.face}),\n              child: Text(\n                '@${item.modules.moduleAuthor.name}',\n                style: authorStyle,\n              ),\n            ),\n            const SizedBox(width: 6),\n            Text(\n              Utils.dateFormat(item.modules.moduleAuthor.pubTs),\n              style: TextStyle(\n                  color: Theme.of(context).colorScheme.outline,\n                  fontSize: Theme.of(context).textTheme.labelSmall!.fontSize),\n            ),\n          ],\n        ),\n      ],\n      const SizedBox(height: 4),\n      if (item.modules.moduleDynamic.topic != null) ...[\n        Padding(\n          padding: floor == 2\n              ? EdgeInsets.zero\n              : const EdgeInsets.only(left: 12, right: 12),\n          child: GestureDetector(\n            child: Text(\n              '#${item.modules.moduleDynamic.topic.name}',\n              style: authorStyle,\n            ),\n          ),\n        ),\n        const SizedBox(height: 6),\n      ],\n      if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[\n        Text.rich(richNode(item, context)),\n        const SizedBox(height: 6),\n      ],\n      GestureDetector(\n        onTap: () {\n          _dynamicsController.pushDetail(item, floor);\n        },\n        child: LayoutBuilder(builder: (context, box) {\n          double width = box.maxWidth;\n          return Stack(\n            children: [\n              Hero(\n                tag: liveRcmd.roomId.toString(),\n                child: NetworkImgLayer(\n                  type: floor == 1 ? 'emote' : null,\n                  width: width,\n                  height: width / StyleString.aspectRatio,\n                  src: item.modules.moduleDynamic.major.liveRcmd.cover,\n                ),\n              ),\n              PBadge(\n                text: watchedShow['text_large'],\n                top: 6,\n                right: 56,\n                bottom: null,\n                left: null,\n                type: 'gray',\n              ),\n              PBadge(\n                text: liveStatus == 1 ? '直播中' : '直播结束',\n                top: 6,\n                right: 6,\n                bottom: null,\n                left: null,\n              ),\n              Positioned(\n                left: 0,\n                right: 0,\n                bottom: 0,\n                child: Container(\n                  height: 80,\n                  padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),\n                  clipBehavior: Clip.hardEdge,\n                  decoration: BoxDecoration(\n                      gradient: const LinearGradient(\n                        begin: Alignment.topCenter,\n                        end: Alignment.bottomCenter,\n                        colors: <Color>[\n                          Colors.transparent,\n                          Colors.black45,\n                        ],\n                      ),\n                      borderRadius: floor == 1\n                          ? null\n                          : const BorderRadius.all(Radius.circular(6))),\n                  child: Row(\n                    mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                    crossAxisAlignment: CrossAxisAlignment.end,\n                    children: [\n                      DefaultTextStyle.merge(\n                        style: TextStyle(\n                            fontSize: Theme.of(context)\n                                .textTheme\n                                .labelMedium!\n                                .fontSize,\n                            color: Colors.white),\n                        child: Row(\n                          children: [\n                            Text(item.modules.moduleDynamic.major.liveRcmd\n                                    .areaName ??\n                                ''),\n                          ],\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            ],\n          );\n        }),\n      ),\n      const SizedBox(height: 6),\n      Padding(\n        padding: floor == 1\n            ? const EdgeInsets.only(left: 12, right: 12)\n            : EdgeInsets.zero,\n        child: Text(\n          item.modules.moduleDynamic.major.liveRcmd.title,\n          maxLines: 1,\n          style: const TextStyle(fontWeight: FontWeight.bold),\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n      const SizedBox(height: 2),\n    ],\n  );\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/pic_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/plugin/pl_gallery/index.dart';\n\nvoid onPreviewImg(currentUrl, picList, initIndex, context) {\n  Navigator.of(context).push(\n    HeroDialogRoute<void>(\n      builder: (BuildContext context) => InteractiveviewerGallery(\n        sources: picList,\n        initIndex: initIndex,\n        onPageChanged: (int pageIndex) {},\n      ),\n    ),\n  );\n}\n\nWidget picWidget(item, context) {\n  String type = item.modules.moduleDynamic.major.type;\n  List pictures = [];\n  if (type == 'MAJOR_TYPE_OPUS') {\n    /// fix 图片跟rich_node_panel重复\n    // pictures = item.modules.moduleDynamic.major.opus.pics;\n    return const SizedBox();\n  }\n  if (type == 'MAJOR_TYPE_DRAW') {\n    pictures = item.modules.moduleDynamic.major.draw.items;\n  }\n  int len = pictures.length;\n  List<String> picList = [];\n  List<Widget> list = [];\n  for (var i = 0; i < len; i++) {\n    picList.add(pictures[i].src ?? pictures[i].url);\n  }\n  for (var i = 0; i < len; i++) {\n    list.add(\n      LayoutBuilder(\n        builder: (context, BoxConstraints box) {\n          return Hero(\n            tag: picList[i],\n            placeholderBuilder:\n                (BuildContext context, Size heroSize, Widget child) {\n              return child;\n            },\n            child: GestureDetector(\n              onTap: () => onPreviewImg(picList[i], picList, i, context),\n              child: NetworkImgLayer(\n                src: pictures[i].src ?? pictures[i].url,\n                width: box.maxWidth,\n                height: box.maxWidth,\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n  return LayoutBuilder(\n    builder: (context, BoxConstraints box) {\n      double maxWidth = box.maxWidth;\n      double aspectRatio = 1.0;\n      double origAspectRatio = 0.0;\n      double crossCount = 3;\n\n      double height = 0.0;\n      if (len == 1) {\n        try {\n          origAspectRatio =\n              aspectRatio = pictures.first.width / pictures.first.height;\n        } catch (_) {}\n        if (aspectRatio < 0.4) {\n          aspectRatio = 0.4;\n        }\n        if (origAspectRatio < 0.5 || pictures.first.width < 1920) {\n          crossCount = 2;\n          height = maxWidth / 2 / aspectRatio;\n        }\n      } else {\n        aspectRatio = 1;\n        height =\n            maxWidth / crossCount * ((len + crossCount - 1) ~/ crossCount) + 6;\n      }\n      return Container(\n        padding: const EdgeInsets.only(top: 4),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(StyleString.imgRadius.x),\n        ),\n        clipBehavior: Clip.hardEdge,\n        height: height,\n        child: Stack(\n          children: [\n            GridView.count(\n              padding: EdgeInsets.zero,\n              physics: const NeverScrollableScrollPhysics(),\n              crossAxisCount: crossCount.toInt(),\n              mainAxisSpacing: 4.0,\n              crossAxisSpacing: 4.0,\n              childAspectRatio: aspectRatio,\n              children: list,\n            ),\n            if (len == 1 && height > Get.size.height * 0.9)\n              const PBadge(\n                text: '长图',\n                top: null,\n                right: null,\n                bottom: 6.0,\n                left: 6.0,\n                type: 'gray',\n              )\n          ],\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/rich_node_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/search.dart';\n\n// 富文本\nInlineSpan richNode(item, context) {\n  final spacer = _VerticalSpaceSpan(0.0);\n  try {\n    TextStyle authorStyle =\n        TextStyle(color: Theme.of(context).colorScheme.primary);\n    List<InlineSpan> spanChilds = [];\n\n    dynamic richTextNodes;\n    if (item.modules.moduleDynamic.desc != null) {\n      richTextNodes = item.modules.moduleDynamic.desc.richTextNodes;\n    } else if (item.modules.moduleDynamic.major != null) {\n      // 动态页面 richTextNodes 层级可能与主页动态层级不同\n      richTextNodes =\n          item.modules.moduleDynamic.major.opus.summary.richTextNodes;\n      if (item.modules.moduleDynamic.major.opus.title != null) {\n        spanChilds.add(\n          TextSpan(\n            text: item.modules.moduleDynamic.major.opus.title + '\\n',\n            style: Theme.of(context)\n                .textTheme\n                .titleMedium!\n                .copyWith(fontWeight: FontWeight.bold),\n          ),\n        );\n      }\n    }\n    if (richTextNodes == null || richTextNodes.isEmpty) {\n      return spacer;\n    } else {\n      for (var i in richTextNodes) {\n        /// fix 渲染专栏时内容会重复\n        // if (item.modules.moduleDynamic.major.opus.title == null &&\n        //     i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {\n        if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {\n          spanChilds.add(\n              TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));\n        }\n        // @用户\n        if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  GestureDetector(\n                    onTap: () => Get.toNamed('/member?mid=${i.rid}',\n                        arguments: {'face': null}),\n                    child: Text(\n                      ' ${i.text}',\n                      style: authorStyle,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          );\n        }\n        // 话题\n        if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: GestureDetector(\n                onTap: () {},\n                child: Text(\n                  '${i.origText}',\n                  style: authorStyle,\n                ),\n              ),\n            ),\n          );\n        }\n        // 网页链接\n        if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: Icon(\n                Icons.link,\n                size: 20,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n            ),\n          );\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: GestureDetector(\n                onTap: () {\n                  Get.toNamed(\n                    '/webview',\n                    parameters: {\n                      'url': i.origText,\n                      'type': 'url',\n                      'pageTitle': ''\n                    },\n                  );\n                },\n                child: Text(\n                  i.text,\n                  style: authorStyle,\n                ),\n              ),\n            ),\n          );\n        }\n        // 投票\n        if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: GestureDetector(\n                onTap: () {\n                  try {\n                    String dynamicId = item.basic['comment_id_str'];\n                    Get.toNamed(\n                      '/webview',\n                      parameters: {\n                        'url':\n                            'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',\n                        'type': 'vote',\n                        'pageTitle': '投票'\n                      },\n                    );\n                  } catch (_) {}\n                },\n                child: Text(\n                  '投票：${i.text}',\n                  style: authorStyle,\n                ),\n              ),\n            ),\n          );\n        }\n        // 表情\n        if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {\n          spanChilds.add(\n            WidgetSpan(\n              child: NetworkImgLayer(\n                src: i.emoji.iconUrl,\n                type: 'emote',\n                width: i.emoji.size * 20,\n                height: i.emoji.size * 20,\n              ),\n            ),\n          );\n        }\n        // 抽奖\n        if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: Icon(\n                Icons.redeem_rounded,\n                size: 16,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n            ),\n          );\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: GestureDetector(\n                onTap: () {},\n                child: Text(\n                  '${i.origText} ',\n                  style: authorStyle,\n                ),\n              ),\n            ),\n          );\n        }\n\n        /// TODO 商品\n        if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: Icon(\n                Icons.shopping_bag_outlined,\n                size: 16,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n            ),\n          );\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: GestureDetector(\n                onTap: () {},\n                child: Text(\n                  '${i.text} ',\n                  style: authorStyle,\n                ),\n              ),\n            ),\n          );\n        }\n        // 投稿\n        if (i.type == 'RICH_TEXT_NODE_TYPE_BV') {\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: Icon(\n                Icons.play_circle_outline_outlined,\n                size: 16,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n            ),\n          );\n          spanChilds.add(\n            WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: GestureDetector(\n                onTap: () async {\n                  try {\n                    int cid = await SearchHttp.ab2c(bvid: i.rid);\n                    Get.toNamed('/video?bvid=${i.rid}&cid=$cid',\n                        arguments: {'pic': null, 'heroTag': i.rid});\n                  } catch (err) {\n                    SmartDialog.showToast(err.toString());\n                  }\n                },\n                child: Text(\n                  '${i.text} ',\n                  style: authorStyle,\n                ),\n              ),\n            ),\n          );\n        }\n      }\n      // if (contentType == 'major' &&\n      //     item.modules.moduleDynamic.major.opus.pics.isNotEmpty) {\n      //   // 图片可能跟其他widget重复渲染\n      //   List<OpusPicsModel> pics = item.modules.moduleDynamic.major.opus.pics;\n      //   int len = pics.length;\n      //   List<String> picList = [];\n\n      //   if (len == 1) {\n      //     OpusPicsModel pictureItem = pics.first;\n      //     picList.add(pictureItem.url!);\n      //     spanChilds.add(const TextSpan(text: '\\n'));\n      //     spanChilds.add(\n      //       WidgetSpan(\n      //         child: LayoutBuilder(\n      //           builder: (context, BoxConstraints box) {\n      //             return GestureDetector(\n      //               onTap: () {\n      //                 showDialog(\n      //                   useSafeArea: false,\n      //                   context: context,\n      //                   builder: (context) {\n      //                     return ImagePreview(initialPage: 0, imgList: picList);\n      //                   },\n      //                 );\n      //               },\n      //               child: Padding(\n      //                 padding: const EdgeInsets.only(top: 4),\n      //                 child: NetworkImgLayer(\n      //                   src: pictureItem.url,\n      //                   width: box.maxWidth / 2,\n      //                   height: box.maxWidth *\n      //                       0.5 *\n      //                       (pictureItem.height != null &&\n      //                               pictureItem.width != null\n      //                           ? pictureItem.height! / pictureItem.width!\n      //                           : 1),\n      //                 ),\n      //               ),\n      //             );\n      //           },\n      //         ),\n      //       ),\n      //     );\n      //   }\n      // if (len > 1) {\n      //   List<Widget> list = [];\n      //   for (var i = 0; i < len; i++) {\n      //     picList.add(pics[i].url!);\n      //     list.add(\n      //       LayoutBuilder(\n      //         builder: (context, BoxConstraints box) {\n      //           return GestureDetector(\n      //             onTap: () {\n      //               showDialog(\n      //                 useSafeArea: false,\n      //                 context: context,\n      //                 builder: (context) {\n      //                   return ImagePreview(initialPage: i, imgList: picList);\n      //                 },\n      //               );\n      //             },\n      //             child: NetworkImgLayer(\n      //               src: pics[i].url,\n      //               width: box.maxWidth,\n      //               height: box.maxWidth,\n      //             ),\n      //           );\n      //         },\n      //       ),\n      //     );\n      //   }\n      //   spanChilds.add(\n      //     WidgetSpan(\n      //       child: LayoutBuilder(\n      //         builder: (context, BoxConstraints box) {\n      //           double maxWidth = box.maxWidth;\n      //           double crossCount = len < 3 ? 2 : 3;\n      //           double height = maxWidth /\n      //                   crossCount *\n      //                   (len % crossCount == 0\n      //                       ? len ~/ crossCount\n      //                       : len ~/ crossCount + 1) +\n      //               6;\n      //           return Container(\n      //             padding: const EdgeInsets.only(top: 6),\n      //             height: height,\n      //             child: GridView.count(\n      //               padding: EdgeInsets.zero,\n      //               physics: const NeverScrollableScrollPhysics(),\n      //               crossAxisCount: crossCount.toInt(),\n      //               mainAxisSpacing: 4.0,\n      //               crossAxisSpacing: 4.0,\n      //               childAspectRatio: 1,\n      //               children: list,\n      //             ),\n      //           );\n      //         },\n      //       ),\n      //     ),\n      //   );\n      // }\n      // spanChilds.add(\n      //   WidgetSpan(\n      //     child: NetworkImgLayer(\n      //       src: pics.first.url,\n      //       type: 'emote',\n      //       width: 100,\n      //       height: 200,\n      //     ),\n      //   ),\n      // );\n      // }\n      return TextSpan(\n        children: spanChilds,\n      );\n    }\n  } catch (err) {\n    print('❌rich_node_panel err: $err');\n    return spacer;\n  }\n}\n\nclass _VerticalSpaceSpan extends WidgetSpan {\n  _VerticalSpaceSpan(double height)\n      : super(child: SizedBox(height: height, width: double.infinity));\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/up_panel.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/dynamics/up.dart';\nimport 'package:pilipala/models/live/item.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass UpPanel extends StatefulWidget {\n  final FollowUpModel upData;\n  final Function? onClickUpCb;\n\n  const UpPanel({\n    super.key,\n    required this.upData,\n    this.onClickUpCb,\n  });\n\n  @override\n  State<UpPanel> createState() => _UpPanelState();\n}\n\nclass _UpPanelState extends State<UpPanel> {\n  final ScrollController scrollController = ScrollController();\n  int currentMid = -1;\n  late double contentWidth = 56;\n  List<UpItem> upList = [];\n  List<LiveUserItem> liveList = [];\n  static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);\n  late MyInfo userInfo;\n\n  void listFormat() {\n    userInfo = widget.upData.myInfo!;\n    upList = widget.upData.upList!;\n    liveList = widget.upData.liveList!;\n  }\n\n  void onClickUp(data, i) {\n    currentMid = data.mid;\n    widget.onClickUpCb?.call(data);\n    // int liveLen = liveList.length;\n    // int upLen = upList.length;\n    // double itemWidth = contentWidth + itemPadding.horizontal;\n    // double screenWidth = MediaQuery.sizeOf(context).width;\n    // double moveDistance = 0.0;\n    // if (itemWidth * (upList.length + liveList.length) <= screenWidth) {\n    // } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {\n    //   moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;\n    // } else {\n    //   moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;\n    // }\n    // data.hasUpdate = false;\n    // scrollController.animateTo(\n    //   moveDistance,\n    //   duration: const Duration(milliseconds: 200),\n    //   curve: Curves.linear,\n    // );\n    // setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    listFormat();\n    return SliverPersistentHeader(\n      floating: true,\n      pinned: false,\n      delegate: _SliverHeaderDelegate(\n          height: liveList.isNotEmpty || upList.isNotEmpty ? 126 : 0,\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.start,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Container(\n                color: Theme.of(context).colorScheme.surface,\n                padding: const EdgeInsets.only(left: 16, right: 16),\n                child: Row(\n                  mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                  crossAxisAlignment: CrossAxisAlignment.center,\n                  children: [\n                    const Text('最新关注'),\n                    GestureDetector(\n                      onTap: () {\n                        feedBack();\n                        Get.toNamed('/follow?mid=${userInfo.mid}');\n                      },\n                      child: Container(\n                        padding: const EdgeInsets.only(top: 5, bottom: 5),\n                        child: Text(\n                          '查看全部',\n                          style: TextStyle(\n                              color: Theme.of(context).colorScheme.outline),\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              Container(\n                height: 90,\n                color: Theme.of(context).colorScheme.surface,\n                child: Row(\n                  children: [\n                    Flexible(\n                      child: ListView(\n                        scrollDirection: Axis.horizontal,\n                        controller: scrollController,\n                        children: [\n                          const SizedBox(width: 10),\n                          if (liveList.isNotEmpty) ...[\n                            for (int i = 0; i < liveList.length; i++) ...[\n                              upItemBuild(liveList[i], i)\n                            ],\n                            VerticalDivider(\n                              indent: 20,\n                              endIndent: 40,\n                              width: 26,\n                              color: Theme.of(context)\n                                  .colorScheme\n                                  .primary\n                                  .withOpacity(0.5),\n                            ),\n                          ],\n                          for (int i = 0; i < upList.length; i++) ...[\n                            upItemBuild(upList[i], i)\n                          ],\n                          const SizedBox(width: 10),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              Container(\n                height: 6,\n                color: Theme.of(context)\n                    .colorScheme\n                    .onInverseSurface\n                    .withOpacity(0.5),\n              ),\n            ],\n          )),\n    );\n  }\n\n  Widget upItemBuild(data, i) {\n    bool isCurrent = currentMid == data.mid || currentMid == -1;\n    return InkWell(\n      onTap: () {\n        feedBack();\n        if (data.type == 'up') {\n          EasyThrottle.throttle('follow', const Duration(milliseconds: 300),\n              () {\n            onClickUp(data, i);\n          });\n        } else if (data.type == 'live') {\n          LiveItemModel liveItem = LiveItemModel.fromJson({\n            'title': data.title,\n            'uname': data.uname,\n            'face': data.face,\n            'roomid': data.roomId,\n          });\n          Get.toNamed(\n            '/liveRoom?roomid=${data.roomId}',\n            arguments: {'liveItem': liveItem},\n          );\n        }\n      },\n      onLongPress: () {\n        feedBack();\n        if (data.mid == -1) {\n          return;\n        }\n        String heroTag = Utils.makeHeroTag(data.mid);\n        Get.toNamed('/member?mid=${data.mid}',\n            arguments: {'face': data.face, 'heroTag': heroTag});\n      },\n      child: Padding(\n        padding: itemPadding,\n        child: AnimatedOpacity(\n          opacity: isCurrent ? 1 : 0.3,\n          duration: const Duration(milliseconds: 200),\n          curve: Curves.easeInOut,\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              Badge(\n                smallSize: 8,\n                label: data.type == 'live' ? const Text('Live') : null,\n                textColor: Theme.of(context).colorScheme.onSecondaryContainer,\n                alignment: data.type == 'live'\n                    ? AlignmentDirectional.topCenter\n                    : AlignmentDirectional.topEnd,\n                padding: const EdgeInsets.only(left: 6, right: 6),\n                isLabelVisible: data.type == 'live' ||\n                    (data.type == 'up' && (data.hasUpdate ?? false)),\n                backgroundColor: data.type == 'live'\n                    ? Theme.of(context).colorScheme.secondaryContainer\n                    : Theme.of(context).colorScheme.primary,\n                child: data.face != ''\n                    ? NetworkImgLayer(\n                        width: 50,\n                        height: 50,\n                        src: data.face,\n                        type: 'avatar',\n                      )\n                    : const CircleAvatar(\n                        radius: 25,\n                        backgroundImage: AssetImage(\n                          'assets/images/noface.jpeg',\n                        ),\n                      ),\n              ),\n              Padding(\n                padding: const EdgeInsets.only(top: 4),\n                child: SizedBox(\n                  width: contentWidth,\n                  child: Text(\n                    data.uname,\n                    overflow: TextOverflow.ellipsis,\n                    softWrap: false,\n                    textAlign: TextAlign.center,\n                    style: TextStyle(\n                        color: currentMid == data.mid\n                            ? Theme.of(context).colorScheme.primary\n                            : Theme.of(context).colorScheme.outline,\n                        fontSize:\n                            Theme.of(context).textTheme.labelMedium!.fontSize),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _SliverHeaderDelegate extends SliverPersistentHeaderDelegate {\n  _SliverHeaderDelegate({required this.height, required this.child});\n\n  final double height;\n  final Widget child;\n\n  @override\n  Widget build(\n      BuildContext context, double shrinkOffset, bool overlapsContent) {\n    return child;\n  }\n\n  @override\n  double get maxExtent => height;\n\n  @override\n  double get minExtent => height;\n\n  @override\n  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>\n      true;\n}\n\nclass UpPanelSkeleton extends StatelessWidget {\n  const UpPanelSkeleton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.builder(\n      scrollDirection: Axis.horizontal,\n      physics: const NeverScrollableScrollPhysics(),\n      itemCount: 10,\n      itemBuilder: ((context, index) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              Container(\n                width: 50,\n                height: 50,\n                decoration: BoxDecoration(\n                  color: Theme.of(context).colorScheme.onInverseSurface,\n                  borderRadius: BorderRadius.circular(50),\n                ),\n              ),\n              Container(\n                margin: const EdgeInsets.only(top: 6),\n                width: 45,\n                height: 12,\n                color: Theme.of(context).colorScheme.onInverseSurface,\n              ),\n            ],\n          ),\n        );\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/dynamics/widgets/video_panel.dart",
    "content": "// 视频or合集\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'rich_node_panel.dart';\n\nWidget videoSeasonWidget(item, context, type, {floor = 1}) {\n  TextStyle authorStyle =\n      TextStyle(color: Theme.of(context).colorScheme.primary);\n  // type archive  ugcSeason\n  // archive 视频/显示发布人\n  // ugcSeason 合集/不显示发布人\n\n  // floor 1 2\n  // 1 投稿视频 铺满 borderRadius 0\n  // 2 转发视频 铺满 borderRadius 6\n  Map<dynamic, dynamic> dynamicProperty = {\n    'ugcSeason': item.modules.moduleDynamic.major.ugcSeason,\n    'archive': item.modules.moduleDynamic.major.archive,\n    'pgc': item.modules.moduleDynamic.major.pgc\n  };\n  dynamic content = dynamicProperty[type];\n\n  return Column(\n    crossAxisAlignment: CrossAxisAlignment.start,\n    mainAxisAlignment: MainAxisAlignment.start,\n    children: [\n      if (floor == 2) ...[\n        Row(\n          children: [\n            GestureDetector(\n              onTap: () => Get.toNamed(\n                  '/member?mid=${item.modules.moduleAuthor.mid}',\n                  arguments: {'face': item.modules.moduleAuthor.face}),\n              child: Text(\n                item.modules.moduleAuthor.type == null\n                    ? '@${item.modules.moduleAuthor.name}'\n                    : item.modules.moduleAuthor.name,\n                style: authorStyle,\n              ),\n            ),\n            const SizedBox(width: 6),\n            Text(\n              item.modules.moduleAuthor.pubTs != null\n                  ? Utils.dateFormat(item.modules.moduleAuthor.pubTs)\n                  : item.modules.moduleAuthor.pubTime,\n              style: TextStyle(\n                  color: Theme.of(context).colorScheme.outline,\n                  fontSize: Theme.of(context).textTheme.labelSmall!.fontSize),\n            ),\n          ],\n        ),\n        const SizedBox(height: 6),\n      ],\n      // const SizedBox(height: 4),\n      /// fix #话题跟content重复\n      // if (item.modules.moduleDynamic.topic != null) ...[\n      //   Padding(\n      //     padding: floor == 2\n      //         ? EdgeInsets.zero\n      //         : const EdgeInsets.only(left: 12, right: 12),\n      //     child: GestureDetector(\n      //       child: Text(\n      //         '#${item.modules.moduleDynamic.topic.name}',\n      //         style: authorStyle,\n      //       ),\n      //     ),\n      //   ),\n      //   const SizedBox(height: 6),\n      // ],\n      if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[\n        Text.rich(richNode(item, context)),\n        const SizedBox(height: 6),\n      ],\n      LayoutBuilder(builder: (context, box) {\n        double width = box.maxWidth;\n        return Stack(\n          children: [\n            NetworkImgLayer(\n              type: floor == 1 ? 'emote' : null,\n              width: width,\n              height: width / StyleString.aspectRatio,\n              src: content.cover,\n            ),\n            if (content.badge != null && content.badge['text'] != null)\n              PBadge(\n                text: content.badge['text'],\n                top: 8.0,\n                right: 10.0,\n                bottom: null,\n                left: null,\n              ),\n            Positioned(\n              left: 0,\n              right: 0,\n              bottom: 0,\n              child: Container(\n                height: 80,\n                padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),\n                clipBehavior: Clip.hardEdge,\n                decoration: BoxDecoration(\n                    gradient: const LinearGradient(\n                      begin: Alignment.topCenter,\n                      end: Alignment.bottomCenter,\n                      colors: <Color>[\n                        Colors.transparent,\n                        Colors.black54,\n                      ],\n                    ),\n                    borderRadius: floor == 1\n                        ? null\n                        : const BorderRadius.all(Radius.circular(6))),\n                child: Row(\n                  mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                  crossAxisAlignment: CrossAxisAlignment.end,\n                  children: [\n                    DefaultTextStyle.merge(\n                      style: TextStyle(\n                          fontSize:\n                              Theme.of(context).textTheme.labelMedium!.fontSize,\n                          color: Colors.white),\n                      child: Row(\n                        children: [\n                          Text(content.durationText ?? ''),\n                          if (content.durationText != null)\n                            const SizedBox(width: 10),\n                          Text(content.stat.play + '次围观'),\n                          const SizedBox(width: 10),\n                          Text(content.stat.danmaku + '条弹幕')\n                        ],\n                      ),\n                    ),\n                    Image.asset(\n                      'assets/images/play.png',\n                      width: 60,\n                      height: 60,\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ],\n        );\n      }),\n      const SizedBox(height: 6),\n      Padding(\n        padding: floor == 1\n            ? const EdgeInsets.only(left: 12, right: 12)\n            : EdgeInsets.zero,\n        child: Text(\n          content.title,\n          maxLines: 1,\n          style: const TextStyle(fontWeight: FontWeight.bold),\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n    ],\n  );\n}\n"
  },
  {
    "path": "lib/pages/emote/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\n\nimport '../../http/reply.dart';\nimport '../../models/video/reply/emote.dart';\n\nclass EmotePanelController extends GetxController\n    with GetTickerProviderStateMixin {\n  late List<PackageItem> emotePackage;\n  late TabController tabController;\n\n  Future getEmote() async {\n    var res = await ReplyHttp.getEmoteList(business: 'reply');\n    if (res['status']) {\n      emotePackage = res['data'].packages;\n      tabController = TabController(length: emotePackage.length, vsync: this);\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/emote/index.dart",
    "content": "library emote;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/emote/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport '../../models/video/reply/emote.dart';\nimport 'controller.dart';\n\nclass EmotePanel extends StatefulWidget {\n  final Function onChoose;\n  const EmotePanel({super.key, required this.onChoose});\n\n  @override\n  State<EmotePanel> createState() => _EmotePanelState();\n}\n\nclass _EmotePanelState extends State<EmotePanel>\n    with AutomaticKeepAliveClientMixin {\n  final EmotePanelController _emotePanelController =\n      Get.put(EmotePanelController());\n  late Future _futureBuilderFuture;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    _futureBuilderFuture = _emotePanelController.getEmote();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return FutureBuilder(\n        future: _futureBuilderFuture,\n        builder: (context, snapshot) {\n          if (snapshot.connectionState == ConnectionState.done) {\n            Map data = snapshot.data as Map;\n            if (data['status']) {\n              List<PackageItem> emotePackage =\n                  _emotePanelController.emotePackage;\n\n              return Column(\n                children: [\n                  Expanded(\n                      child: TabBarView(\n                    controller: _emotePanelController.tabController,\n                    children: emotePackage.map(\n                      (e) {\n                        int size = e.emote!.first.meta!.size!;\n                        int type = e.type!;\n                        return Padding(\n                          padding: const EdgeInsets.fromLTRB(12, 6, 12, 0),\n                          child: GridView.builder(\n                            gridDelegate:\n                                SliverGridDelegateWithMaxCrossAxisExtent(\n                              maxCrossAxisExtent: size == 1 ? 40 : 60,\n                              crossAxisSpacing: 8,\n                              mainAxisSpacing: 8,\n                            ),\n                            itemCount: e.emote!.length,\n                            itemBuilder: (context, index) {\n                              return Material(\n                                color: Colors.transparent,\n                                clipBehavior: Clip.hardEdge,\n                                shape: RoundedRectangleBorder(\n                                  borderRadius: BorderRadius.circular(4),\n                                ),\n                                child: InkWell(\n                                  onTap: () {\n                                    widget.onChoose(e, e.emote![index]);\n                                  },\n                                  child: Padding(\n                                    padding: const EdgeInsets.all(3),\n                                    child: type == 4\n                                        ? Text(\n                                            e.emote![index].text!,\n                                            overflow: TextOverflow.clip,\n                                            maxLines: 1,\n                                          )\n                                        : Image.network(\n                                            e.emote![index].url!,\n                                            width: size * 38,\n                                            height: size * 38,\n                                          ),\n                                  ),\n                                ),\n                              );\n                            },\n                          ),\n                        );\n                      },\n                    ).toList(),\n                  )),\n                  Divider(\n                    height: 1,\n                    color: Theme.of(context).dividerColor.withOpacity(0.1),\n                  ),\n                  TabBar(\n                    controller: _emotePanelController.tabController,\n                    dividerColor: Colors.transparent,\n                    isScrollable: true,\n                    tabs: _emotePanelController.emotePackage\n                        .map((e) => Tab(text: e.text))\n                        .toList(),\n                  ),\n                  SizedBox(height: MediaQuery.of(context).padding.bottom + 20),\n                ],\n              );\n            } else {\n              return Center(child: Text(data['msg']));\n            }\n          } else {\n            return const Center(child: Text('加载中...'));\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "lib/pages/fan/controller.dart",
    "content": "import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/fan.dart';\nimport 'package:pilipala/models/fans/result.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass FansController extends GetxController {\n  Box userInfoCache = GStrorage.userInfo;\n  int pn = 1;\n  int ps = 20;\n  int total = 0;\n  RxList<FansItemModel> fansList = <FansItemModel>[].obs;\n  late int mid;\n  late String name;\n  var userInfo;\n  RxString loadingText = '加载中...'.obs;\n  RxBool isOwner = false.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n    mid = Get.parameters['mid'] != null\n        ? int.parse(Get.parameters['mid']!)\n        : userInfo.mid;\n    isOwner.value = mid == userInfo.mid;\n    name = Get.parameters['name'] ?? userInfo.uname;\n  }\n\n  Future queryFans(type) async {\n    if (type == 'init') {\n      pn = 1;\n      loadingText.value == '加载中...';\n    }\n    if (loadingText.value == '没有更多了') {\n      return;\n    }\n    var res = await FanHttp.fans(\n      vmid: mid,\n      pn: pn,\n      ps: ps,\n      orderType: 'attention',\n    );\n    if (res['status']) {\n      if (type == 'init') {\n        fansList.value = res['data'].list;\n        total = res['data'].total;\n      } else if (type == 'onLoad') {\n        fansList.addAll(res['data'].list);\n      }\n      if ((pn == 1 && total < ps) || res['data'].list.isEmpty) {\n        loadingText.value = '没有更多了';\n      }\n      pn += 1;\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/fan/index.dart",
    "content": "library fan;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/fan/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/models/fans/result.dart';\n\nimport 'controller.dart';\nimport 'widgets/fan_item.dart';\n\nclass FansPage extends StatefulWidget {\n  const FansPage({super.key});\n\n  @override\n  State<FansPage> createState() => _FansPageState();\n}\n\nclass _FansPageState extends State<FansPage> {\n  late String mid;\n  late FansController _fansController;\n  final ScrollController scrollController = ScrollController();\n  Future? _futureBuilderFuture;\n\n  @override\n  void initState() {\n    super.initState();\n    mid = Get.parameters['mid']!;\n    _fansController = Get.put(FansController(), tag: mid);\n    _futureBuilderFuture = _fansController.queryFans('init');\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('follow', const Duration(seconds: 1), () {\n            _fansController.queryFans('onLoad');\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        elevation: 0,\n        scrolledUnderElevation: 0,\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          _fansController.isOwner.value ? '我的粉丝' : '${_fansController.name}的粉丝',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async => await _fansController.queryFans('init'),\n        child: FutureBuilder(\n          future: _futureBuilderFuture,\n          builder: (context, snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              var data = snapshot.data;\n              if (data['status']) {\n                List<FansItemModel> list = _fansController.fansList;\n                return Obx(\n                  () => list.isNotEmpty\n                      ? ListView.builder(\n                          controller: scrollController,\n                          itemCount: list.length + 1,\n                          itemBuilder: (BuildContext context, int index) {\n                            if (index == list.length) {\n                              return Container(\n                                height:\n                                    MediaQuery.of(context).padding.bottom + 60,\n                                padding: EdgeInsets.only(\n                                    bottom:\n                                        MediaQuery.of(context).padding.bottom),\n                                child: Center(\n                                  child: Obx(\n                                    () => Text(\n                                      _fansController.loadingText.value,\n                                      style: TextStyle(\n                                          color: Theme.of(context)\n                                              .colorScheme\n                                              .outline,\n                                          fontSize: 13),\n                                    ),\n                                  ),\n                                ),\n                              );\n                            } else {\n                              return fanItem(item: list[index]);\n                            }\n                          },\n                        )\n                      : const CustomScrollView(\n                          slivers: [NoData()],\n                        ),\n                );\n              } else {\n                return CustomScrollView(\n                  physics: const NeverScrollableScrollPhysics(),\n                  slivers: [\n                    HttpError(\n                      errMsg: data['msg'],\n                      fn: () {\n                        _futureBuilderFuture =\n                            _fansController.queryFans('init');\n                      },\n                    )\n                  ],\n                );\n              }\n            } else {\n              // 骨架屏\n              return const SizedBox();\n            }\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/fan/widgets/fan_item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nWidget fanItem({item}) {\n  String heroTag = Utils.makeHeroTag(item!.mid);\n  return ListTile(\n    onTap: () => Get.toNamed('/member?mid=${item.mid}',\n        arguments: {'face': item.face, 'heroTag': heroTag}),\n    leading: Hero(\n      tag: heroTag,\n      child: NetworkImgLayer(\n        width: 38,\n        height: 38,\n        type: 'avatar',\n        src: item.face,\n      ),\n    ),\n    title: Text(item.uname),\n    subtitle: Text(\n      item.sign,\n      maxLines: 1,\n      overflow: TextOverflow.ellipsis,\n    ),\n    dense: true,\n    trailing: const SizedBox(width: 6),\n  );\n}\n"
  },
  {
    "path": "lib/pages/fav/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/user/fav_folder.dart';\nimport 'package:pilipala/models/user/info.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass FavController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  Rx<FavFolderData> favFolderData = FavFolderData().obs;\n  RxList<FavFolderItemData> favFolderList = <FavFolderItemData>[].obs;\n  Box userInfoCache = GStrorage.userInfo;\n  UserInfoData? userInfo;\n  int currentPage = 1;\n  int pageSize = 60;\n  RxBool hasMore = true.obs;\n  late int mid;\n  late int ownerMid;\n  RxBool isOwner = false.obs;\n\n  @override\n  void onInit() {\n    mid = int.parse(Get.parameters['mid'] ?? '-1');\n    userInfo = userInfoCache.get('userInfoCache');\n    ownerMid = userInfo != null ? userInfo!.mid! : -1;\n    isOwner.value = mid == -1 || mid == ownerMid;\n    super.onInit();\n  }\n\n  Future<dynamic> queryFavFolder({type = 'init'}) async {\n    if (userInfo == null) {\n      return {'status': false, 'msg': '账号未登录', 'code': -101};\n    }\n    if (!hasMore.value) {\n      return;\n    }\n    var res = await UserHttp.userfavFolder(\n      pn: currentPage,\n      ps: pageSize,\n      mid: isOwner.value ? ownerMid : mid,\n    );\n    if (res['status']) {\n      if (type == 'init') {\n        favFolderData.value = res['data'];\n        favFolderList.value = res['data'].list;\n      } else {\n        if (res['data'].list.isNotEmpty) {\n          favFolderList.addAll(res['data'].list);\n          favFolderData.update((val) {});\n        }\n      }\n      hasMore.value = res['data'].hasMore;\n      currentPage++;\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n    return res;\n  }\n\n  Future onLoad() async {\n    queryFavFolder(type: 'onload');\n  }\n\n  removeFavFolder({required int mediaIds}) async {\n    for (var i in favFolderList) {\n      if (i.id == mediaIds) {\n        favFolderList.remove(i);\n        break;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav/index.dart",
    "content": "library fav;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/fav/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/pages/fav/index.dart';\nimport 'package:pilipala/pages/fav/widgets/item.dart';\nimport 'package:pilipala/utils/route_push.dart';\n\nclass FavPage extends StatefulWidget {\n  const FavPage({super.key});\n\n  @override\n  State<FavPage> createState() => _FavPageState();\n}\n\nclass _FavPageState extends State<FavPage> {\n  final FavController _favController = Get.put(FavController());\n  late Future _futureBuilderFuture;\n  late ScrollController scrollController;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _favController.queryFavFolder();\n    scrollController = _favController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('history', const Duration(seconds: 1), () {\n            _favController.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Obx(() => Text(\n              '${_favController.isOwner.value ? '我' : 'Ta'}的收藏',\n              style: Theme.of(context).textTheme.titleMedium,\n            )),\n        actions: [\n          Obx(() => !_favController.isOwner.value\n              ? IconButton(\n                  onPressed: () =>\n                      Get.toNamed('/subscription?mid=${_favController.mid}'),\n                  icon: const Icon(Icons.subscriptions_outlined, size: 21),\n                  tooltip: 'Ta的订阅',\n                )\n              : const SizedBox.shrink()),\n\n          // 新建收藏夹\n          Obx(() => _favController.isOwner.value\n              ? IconButton(\n                  onPressed: () async {\n                    await Get.toNamed('/favEdit');\n                    _favController.hasMore.value = true;\n                    _favController.currentPage = 1;\n                    setState(() {\n                      _futureBuilderFuture = _favController.queryFavFolder();\n                    });\n                  },\n                  icon: const Icon(Icons.add_outlined),\n                  tooltip: '新建收藏夹',\n                )\n              : const SizedBox.shrink()),\n          IconButton(\n            onPressed: () => Get.toNamed(\n                '/favSearch?searchType=1&mediaId=${_favController.favFolderData.value.list!.first.id}'),\n            icon: const Icon(Icons.search_outlined),\n          ),\n          const SizedBox(width: 14),\n        ],\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async {\n          _favController.hasMore.value = true;\n          _favController.currentPage = 1;\n          setState(() {\n            _futureBuilderFuture = _favController.queryFavFolder(type: 'init');\n          });\n        },\n        child: _buildBody(),\n      ),\n    );\n  }\n\n  Widget _buildBody() {\n    return FutureBuilder(\n      future: _futureBuilderFuture,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == ConnectionState.done) {\n          Map? data = snapshot.data;\n          if (data != null && data['status']) {\n            return Obx(\n              () => ListView.builder(\n                controller: scrollController,\n                itemCount: _favController.favFolderList.length,\n                itemBuilder: (context, index) {\n                  return FavItem(\n                    favFolderItem: _favController.favFolderList[index],\n                    isOwner: _favController.isOwner.value,\n                  );\n                },\n              ),\n            );\n          } else {\n            return CustomScrollView(\n              physics: const NeverScrollableScrollPhysics(),\n              slivers: [\n                HttpError(\n                  errMsg: data?['msg'] ?? '请求异常',\n                  btnText: data?['code'] == -101 ? '去登录' : null,\n                  fn: () {\n                    if (data?['code'] == -101) {\n                      RoutePush.loginRedirectPush();\n                    } else {\n                      setState(() {\n                        _futureBuilderFuture = _favController.queryFavFolder();\n                      });\n                    }\n                  },\n                ),\n              ],\n            );\n          }\n        } else {\n          // 骨架屏\n          return ListView.builder(\n            itemBuilder: (context, index) {\n              return const VideoCardHSkeleton();\n            },\n            itemCount: 10,\n          );\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav/widgets/item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass FavItem extends StatelessWidget {\n  // ignore: prefer_typing_uninitialized_variables\n  final favFolderItem;\n  final bool isOwner;\n  const FavItem(\n      {super.key, required this.favFolderItem, required this.isOwner});\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(favFolderItem.fid);\n    return InkWell(\n      onTap: () async {\n        Get.toNamed(\n          '/favDetail',\n          arguments: favFolderItem,\n          parameters: {\n            'heroTag': heroTag,\n            'mediaId': favFolderItem.id.toString(),\n            'isOwner': isOwner ? '1' : '0',\n          },\n        );\n      },\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(12, 7, 12, 7),\n        child: LayoutBuilder(\n          builder: (context, boxConstraints) {\n            double width =\n                (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;\n            return SizedBox(\n              height: width / StyleString.aspectRatio,\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.start,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  AspectRatio(\n                    aspectRatio: StyleString.aspectRatio,\n                    child: LayoutBuilder(\n                      builder: (context, boxConstraints) {\n                        double maxWidth = boxConstraints.maxWidth;\n                        double maxHeight = boxConstraints.maxHeight;\n                        return Hero(\n                          tag: heroTag,\n                          child: NetworkImgLayer(\n                            src: favFolderItem.cover,\n                            width: maxWidth,\n                            height: maxHeight,\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                  VideoContent(favFolderItem: favFolderItem)\n                ],\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass VideoContent extends StatelessWidget {\n  final dynamic favFolderItem;\n  const VideoContent({super.key, required this.favFolderItem});\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(10, 2, 6, 10),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              favFolderItem.title,\n              textAlign: TextAlign.start,\n              style: const TextStyle(\n                fontWeight: FontWeight.w500,\n                letterSpacing: 0.3,\n              ),\n            ),\n            Text(\n              '${favFolderItem.mediaCount}个内容',\n              textAlign: TextAlign.start,\n              style: TextStyle(\n                fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                color: Theme.of(context).colorScheme.outline,\n              ),\n            ),\n            const Spacer(),\n            Text(\n              [23, 1].contains(favFolderItem.attr) ? '私密' : '公开',\n              textAlign: TextAlign.start,\n              style: TextStyle(\n                fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                color: Theme.of(context).colorScheme.outline,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav_detail/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/user/fav_detail.dart';\nimport 'package:pilipala/models/user/fav_folder.dart';\nimport 'package:pilipala/pages/fav/index.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass FavDetailController extends GetxController {\n  FavFolderItemData? item;\n  RxString title = ''.obs;\n\n  int? mediaId;\n  late String heroTag;\n  int currentPage = 1;\n  bool isLoadingMore = false;\n  RxMap favInfo = {}.obs;\n  RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;\n  RxString loadingText = '加载中...'.obs;\n  RxInt mediaCount = 0.obs;\n  late String isOwner;\n\n  @override\n  void onInit() {\n    item = Get.arguments;\n    title.value = item!.title!;\n    if (Get.parameters.keys.isNotEmpty) {\n      mediaId = int.parse(Get.parameters['mediaId']!);\n      heroTag = Get.parameters['heroTag']!;\n      isOwner = Get.parameters['isOwner']!;\n    }\n    super.onInit();\n  }\n\n  Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async {\n    if (type == 'onLoad' && favList.length >= mediaCount.value) {\n      loadingText.value = '没有更多了';\n      return;\n    }\n    isLoadingMore = true;\n    var res = await UserHttp.userFavFolderDetail(\n      pn: currentPage,\n      ps: 20,\n      mediaId: mediaId!,\n    );\n    if (res['status']) {\n      favInfo.value = res['data'].info;\n      if (currentPage == 1 && type == 'init') {\n        favList.value = res['data'].medias;\n        mediaCount.value = res['data'].info['media_count'];\n      } else if (type == 'onLoad') {\n        favList.addAll(res['data'].medias);\n      }\n      if (favList.length >= mediaCount.value) {\n        loadingText.value = '没有更多了';\n      }\n    }\n    currentPage += 1;\n    isLoadingMore = false;\n    return res;\n  }\n\n  onCancelFav(int id) async {\n    var result = await VideoHttp.favVideo(\n        aid: id, addIds: '', delIds: mediaId.toString());\n    if (result['status']) {\n      List dataList = favList;\n      for (var i in dataList) {\n        if (i.id == id) {\n          dataList.remove(i);\n          break;\n        }\n      }\n      SmartDialog.showToast('取消收藏');\n    }\n  }\n\n  onLoad() {\n    queryUserFavFolderDetail(type: 'onLoad');\n  }\n\n  onDelFavFolder() async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: const Text('确定删除这个收藏夹吗？'),\n          actions: [\n            TextButton(\n              onPressed: () async {\n                SmartDialog.dismiss();\n              },\n              child: Text(\n                '点错了',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                var res = await UserHttp.delFavFolder(mediaIds: mediaId!);\n                SmartDialog.dismiss();\n                SmartDialog.showToast(res['status'] ? '操作成功' : res['msg']);\n                if (res['status']) {\n                  FavController favController = Get.find<FavController>();\n                  await favController.removeFavFolder(mediaIds: mediaId!);\n                  Get.back();\n                }\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  onEditFavFolder() async {\n    var res = await Get.toNamed(\n      '/favEdit',\n      arguments: {\n        'mediaId': mediaId.toString(),\n        'title': item!.title,\n        'intro': item!.intro,\n        'cover': item!.cover,\n        'privacy': [23, 1].contains(item!.attr) ? 1 : 0,\n      },\n    );\n    title.value = res['title'];\n    print(title);\n  }\n\n  Future toViewPlayAll() async {\n    final FavDetailItemData firstItem = favList.first;\n    final String heroTag = Utils.makeHeroTag(firstItem.bvid);\n    Get.toNamed(\n      '/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',\n      arguments: {\n        'videoItem': firstItem,\n        'heroTag': heroTag,\n        'sourceType': 'fav',\n        'mediaId': favInfo['id'],\n        'oid': firstItem.id,\n        'favTitle': favInfo['title'],\n        'favInfo': favInfo,\n        'count': favInfo['media_count'],\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav_detail/index.dart",
    "content": "library favdetail;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/fav_detail/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/pages/fav_detail/index.dart';\n\nimport 'widget/fav_video_card.dart';\n\nclass FavDetailPage extends StatefulWidget {\n  const FavDetailPage({super.key});\n\n  @override\n  State<FavDetailPage> createState() => _FavDetailPageState();\n}\n\nclass _FavDetailPageState extends State<FavDetailPage> {\n  late final ScrollController _controller = ScrollController();\n  final FavDetailController _favDetailController =\n      Get.put(FavDetailController());\n  late StreamController<bool> titleStreamC =\n      StreamController<bool>.broadcast(); // a\n  Future? _futureBuilderFuture;\n  late String mediaId;\n\n  @override\n  void initState() {\n    super.initState();\n    mediaId = Get.parameters['mediaId']!;\n    _futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();\n    _controller.addListener(\n      () {\n        if (_controller.offset > 160) {\n          titleStreamC.add(true);\n        } else if (_controller.offset <= 160) {\n          titleStreamC.add(false);\n        }\n\n        if (_controller.position.pixels >=\n            _controller.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('favDetail', const Duration(seconds: 1), () {\n            _favDetailController.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    titleStreamC.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: CustomScrollView(\n        controller: _controller,\n        slivers: [\n          SliverAppBar(\n            expandedHeight: 260 - MediaQuery.of(context).padding.top,\n            pinned: true,\n            titleSpacing: 0,\n            title: StreamBuilder(\n              stream: titleStreamC.stream.distinct(),\n              initialData: false,\n              builder: (context, AsyncSnapshot snapshot) {\n                return AnimatedOpacity(\n                  opacity: snapshot.data ? 1 : 0,\n                  curve: Curves.easeOut,\n                  duration: const Duration(milliseconds: 500),\n                  child: Row(\n                    children: [\n                      Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          Obx(\n                            () => Text(\n                              _favDetailController.title.value,\n                              style: Theme.of(context).textTheme.titleMedium,\n                            ),\n                          ),\n                          Text(\n                            '共${_favDetailController.mediaCount}条视频',\n                            style: Theme.of(context).textTheme.labelMedium,\n                          )\n                        ],\n                      )\n                    ],\n                  ),\n                );\n              },\n            ),\n            actions: [\n              IconButton(\n                onPressed: () =>\n                    Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'),\n                icon: const Icon(Icons.search_outlined),\n              ),\n              PopupMenuButton<String>(\n                icon: const Icon(Icons.more_vert_outlined),\n                position: PopupMenuPosition.under,\n                onSelected: (String type) {},\n                itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[\n                  PopupMenuItem<String>(\n                    onTap: () => _favDetailController.onEditFavFolder(),\n                    value: 'edit',\n                    child: const Text('编辑收藏夹'),\n                  ),\n                  PopupMenuItem<String>(\n                    onTap: () => _favDetailController.onDelFavFolder(),\n                    value: 'pause',\n                    child: const Text('删除收藏夹'),\n                  ),\n                ],\n              ),\n              const SizedBox(width: 14),\n            ],\n            flexibleSpace: FlexibleSpaceBar(\n              background: Container(\n                decoration: BoxDecoration(\n                  border: Border(\n                    bottom: BorderSide(\n                      color: Theme.of(context).dividerColor.withOpacity(0.2),\n                    ),\n                  ),\n                ),\n                padding: EdgeInsets.only(\n                    top: kTextTabBarHeight +\n                        MediaQuery.of(context).padding.top +\n                        30,\n                    left: 20,\n                    right: 20),\n                child: SizedBox(\n                  height: 200,\n                  child: Row(\n                    // mainAxisAlignment: MainAxisAlignment.center,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Hero(\n                        tag: _favDetailController.heroTag,\n                        child: NetworkImgLayer(\n                          width: 180,\n                          height: 110,\n                          src: _favDetailController.item!.cover,\n                        ),\n                      ),\n                      const SizedBox(width: 14),\n                      Expanded(\n                        child: Column(\n                          mainAxisAlignment: MainAxisAlignment.start,\n                          crossAxisAlignment: CrossAxisAlignment.start,\n                          children: [\n                            const SizedBox(height: 4),\n                            Obx(\n                              () => Text(\n                                _favDetailController.title.value,\n                                style: TextStyle(\n                                    fontSize: Theme.of(context)\n                                        .textTheme\n                                        .titleMedium!\n                                        .fontSize,\n                                    fontWeight: FontWeight.bold),\n                              ),\n                            ),\n                            const SizedBox(height: 4),\n                            Text(\n                              _favDetailController.item!.upper!.name!,\n                              style: TextStyle(\n                                  fontSize: Theme.of(context)\n                                      .textTheme\n                                      .labelSmall!\n                                      .fontSize,\n                                  color: Theme.of(context).colorScheme.outline),\n                            )\n                          ],\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n          ),\n          SliverToBoxAdapter(\n            child: Padding(\n              padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),\n              child: Obx(\n                () => Text(\n                  '共${_favDetailController.mediaCount}条视频',\n                  style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline,\n                      letterSpacing: 1),\n                ),\n              ),\n            ),\n          ),\n          FutureBuilder(\n            future: _futureBuilderFuture,\n            builder: (context, snapshot) {\n              if (snapshot.connectionState == ConnectionState.done) {\n                Map data = snapshot.data;\n                if (data['status']) {\n                  if (_favDetailController.item!.mediaCount == 0) {\n                    return const NoData();\n                  } else {\n                    List favList = _favDetailController.favList;\n                    return Obx(\n                      () => favList.isEmpty\n                          ? const SliverToBoxAdapter(child: SizedBox())\n                          : SliverList(\n                              delegate:\n                                  SliverChildBuilderDelegate((context, index) {\n                                return FavVideoCardH(\n                                  videoItem: favList[index],\n                                  isOwner: _favDetailController.isOwner,\n                                  callFn: () => _favDetailController\n                                      .onCancelFav(favList[index].id),\n                                );\n                              }, childCount: favList.length),\n                            ),\n                    );\n                  }\n                } else {\n                  return HttpError(\n                    errMsg: data['msg'],\n                    fn: () => setState(() {}),\n                  );\n                }\n              } else {\n                // 骨架屏\n                return SliverList(\n                  delegate: SliverChildBuilderDelegate((context, index) {\n                    return const VideoCardHSkeleton();\n                  }, childCount: 10),\n                );\n              }\n            },\n          ),\n          SliverToBoxAdapter(\n            child: Container(\n              height: MediaQuery.of(context).padding.bottom + 60,\n              padding: EdgeInsets.only(\n                  bottom: MediaQuery.of(context).padding.bottom),\n              child: Center(\n                child: Obx(\n                  () => Text(\n                    _favDetailController.loadingText.value,\n                    style: TextStyle(\n                        color: Theme.of(context).colorScheme.outline,\n                        fontSize: 13),\n                  ),\n                ),\n              ),\n            ),\n          )\n        ],\n      ),\n      floatingActionButton: Obx(\n        () => _favDetailController.mediaCount > 0\n            ? FloatingActionButton.extended(\n                onPressed: _favDetailController.toViewPlayAll,\n                label: const Text('播放全部'),\n                icon: const Icon(Icons.playlist_play),\n              )\n            : const SizedBox(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav_detail/widget/fav_video_card.dart",
    "content": "import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/stat/danmu.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'package:pilipala/utils/image_save.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport '../../../common/widgets/badge.dart';\n\n// 收藏视频卡片 - 水平布局\nclass FavVideoCardH extends StatelessWidget {\n  final dynamic videoItem;\n  final Function? callFn;\n  final int? searchType;\n  final String isOwner;\n\n  const FavVideoCardH({\n    Key? key,\n    required this.videoItem,\n    this.callFn,\n    this.searchType,\n    required this.isOwner,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    int id = videoItem.id;\n    String bvid = videoItem.bvid ?? IdUtils.av2bv(id);\n    String heroTag = Utils.makeHeroTag(id);\n    return InkWell(\n      onTap: () async {\n        // int? seasonId;\n        String? epId;\n        if (videoItem.ogv != null &&\n            (videoItem.ogv['type_name'] == '番剧' ||\n                videoItem.ogv['type_name'] == '国创')) {\n          videoItem.cid = await SearchHttp.ab2c(bvid: bvid);\n          // seasonId = videoItem.ogv['season_id'];\n          epId = videoItem.epId;\n        } else if (videoItem.page == 0 || videoItem.page > 1) {\n          var result = await VideoHttp.videoIntro(bvid: bvid);\n          if (result['status']) {\n            epId = result['data'].epId;\n          }\n        }\n\n        Map<String, String> parameters = {\n          'bvid': bvid,\n          'cid': videoItem.cid.toString(),\n          'epId': epId ?? '',\n        };\n        // if (seasonId != null) {\n        //   parameters['seasonId'] = seasonId.toString();\n        // }\n        Get.toNamed('/video', parameters: parameters, arguments: {\n          'videoItem': videoItem,\n          'heroTag': heroTag,\n          'videoType':\n              epId != null ? SearchType.media_bangumi : SearchType.video,\n        });\n      },\n      onLongPress: () => imageSaveDialog(\n        context,\n        videoItem,\n        SmartDialog.dismiss,\n      ),\n      child: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.fromLTRB(\n                StyleString.safeSpace, 5, StyleString.safeSpace, 5),\n            child: LayoutBuilder(\n              builder: (context, boxConstraints) {\n                double width =\n                    (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;\n                return SizedBox(\n                  height: width / StyleString.aspectRatio,\n                  child: Row(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      AspectRatio(\n                        aspectRatio: StyleString.aspectRatio,\n                        child: LayoutBuilder(\n                          builder: (context, boxConstraints) {\n                            double maxWidth = boxConstraints.maxWidth;\n                            double maxHeight = boxConstraints.maxHeight;\n                            return Stack(\n                              children: [\n                                Hero(\n                                  tag: heroTag,\n                                  child: NetworkImgLayer(\n                                    src: videoItem.pic,\n                                    width: maxWidth,\n                                    height: maxHeight,\n                                  ),\n                                ),\n                                PBadge(\n                                  text: Utils.timeFormat(videoItem.duration!),\n                                  right: 6.0,\n                                  bottom: 6.0,\n                                  type: 'gray',\n                                ),\n                                if (videoItem.ogv != null) ...[\n                                  PBadge(\n                                    text: videoItem.ogv['type_name'],\n                                    top: 6.0,\n                                    right: 6.0,\n                                    bottom: null,\n                                    left: null,\n                                  ),\n                                ],\n                              ],\n                            );\n                          },\n                        ),\n                      ),\n                      VideoContent(\n                        videoItem: videoItem,\n                        callFn: callFn,\n                        searchType: searchType,\n                        isOwner: isOwner,\n                      )\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass VideoContent extends StatelessWidget {\n  final dynamic videoItem;\n  final Function? callFn;\n  final int? searchType;\n  final String isOwner;\n  const VideoContent({\n    super.key,\n    required this.videoItem,\n    this.callFn,\n    this.searchType,\n    required this.isOwner,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),\n        child: Stack(\n          children: [\n            Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                  videoItem.title,\n                  textAlign: TextAlign.start,\n                  style: const TextStyle(\n                    fontWeight: FontWeight.w500,\n                    letterSpacing: 0.3,\n                  ),\n                  maxLines: 2,\n                  overflow: TextOverflow.ellipsis,\n                ),\n                if (videoItem.ogv != null) ...[\n                  Text(\n                    videoItem.intro,\n                    style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                  ),\n                ],\n                const Spacer(),\n                Text(\n                  Utils.dateFormat(videoItem.favTime),\n                  style: TextStyle(\n                      fontSize: 11,\n                      color: Theme.of(context).colorScheme.outline),\n                ),\n                if (videoItem.owner.name != '') ...[\n                  Text(\n                    videoItem.owner.name,\n                    style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                  ),\n                ],\n                Padding(\n                  padding: const EdgeInsets.only(top: 2),\n                  child: Row(\n                    children: [\n                      StatView(view: videoItem.cntInfo['play']),\n                      const SizedBox(width: 8),\n                      StatDanMu(danmu: videoItem.cntInfo['danmaku']),\n                      const Spacer(),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n            searchType != 1 && isOwner == '1'\n                ? Positioned(\n                    right: 0,\n                    bottom: -4,\n                    child: IconButton(\n                      style: ButtonStyle(\n                        padding: MaterialStateProperty.all(EdgeInsets.zero),\n                      ),\n                      onPressed: () {\n                        showDialog(\n                          context: Get.context!,\n                          builder: (context) {\n                            return AlertDialog(\n                              title: const Text('提示'),\n                              content: const Text('要取消收藏吗?'),\n                              actions: [\n                                TextButton(\n                                    onPressed: () => Get.back(),\n                                    child: Text(\n                                      '取消',\n                                      style: TextStyle(\n                                          color: Theme.of(context)\n                                              .colorScheme\n                                              .outline),\n                                    )),\n                                TextButton(\n                                  onPressed: () async {\n                                    await callFn!();\n                                    Get.back();\n                                  },\n                                  child: const Text('确定取消'),\n                                )\n                              ],\n                            );\n                          },\n                        );\n                      },\n                      icon: Icon(\n                        Icons.clear_outlined,\n                        color: Theme.of(context).colorScheme.outline,\n                        size: 18,\n                      ),\n                    ),\n                  )\n                : const SizedBox(),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav_edit/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/fav.dart';\n\nclass FavEditController extends GetxController {\n  final GlobalKey formKey = GlobalKey<FormState>();\n  final TextEditingController titleController = TextEditingController();\n  final TextEditingController contentController = TextEditingController();\n\n  final FocusNode titleTextFieldNode = FocusNode();\n  final FocusNode contentTextFieldNode = FocusNode();\n\n  // 默认新建\n  RxString type = 'add'.obs;\n\n  String? mediaId;\n  String cover = ''; // 封面\n  String title = ''; // 名称\n  String intro = ''; // 简介\n  RxInt privacy = 0.obs; // 是否公开 0公开 1私密\n\n  @override\n  void onInit() {\n    super.onInit();\n    var args = Get.arguments;\n    if (args != null) {\n      type.value = 'edit';\n      mediaId = args['mediaId'];\n      title = args['title'];\n      intro = args['intro'];\n      cover = args['cover'];\n      privacy.value = args['privacy'];\n      titleController.text = title;\n      contentController.text = intro;\n    }\n  }\n\n  void onSubmit() async {\n    // 表单验证\n    if ((formKey.currentState as FormState).validate()) {\n      if (type.value == 'edit') {\n        await editFolder();\n      } else {\n        await addFolder();\n      }\n    }\n  }\n\n  Future<void> editFolder() async {\n    var res = await FavHttp.editFolder(\n      title: title,\n      intro: intro,\n      mediaId: mediaId!,\n      cover: cover,\n    );\n    if (res['status']) {\n      SmartDialog.showToast('编辑成功');\n      Get.back(result: {'title': title});\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  Future<void> addFolder() async {\n    var res = await FavHttp.addFolder(\n      title: title,\n      intro: intro,\n    );\n    if (res['status']) {\n      SmartDialog.showToast('新建成功');\n      Get.back();\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav_edit/index.dart",
    "content": "library fav_edit;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/fav_edit/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\n\nimport 'controller.dart';\n\nclass FavEditPage extends StatefulWidget {\n  const FavEditPage({super.key});\n\n  @override\n  State<FavEditPage> createState() => _FavEditPageState();\n}\n\nclass _FavEditPageState extends State<FavEditPage> {\n  final FavEditController _favEditController = Get.put(FavEditController());\n  String title = '';\n  String content = '';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        elevation: 0,\n        scrolledUnderElevation: 0,\n        title: Obx(\n          () => _favEditController.type.value == 'add'\n              ? Text(\n                  '新建收藏夹',\n                  style: Theme.of(context).textTheme.titleMedium,\n                )\n              : Text(\n                  '编辑收藏夹',\n                  style: Theme.of(context).textTheme.titleMedium,\n                ),\n        ),\n        centerTitle: false,\n        actions: [\n          Obx(\n            () => _favEditController.privacy.value == 0\n                ? IconButton(\n                    onPressed: () {\n                      _favEditController.privacy.value = 1;\n                    },\n                    icon: const Icon(Icons.lock_open_outlined))\n                : IconButton(\n                    onPressed: () {\n                      _favEditController.privacy.value = 0;\n                    },\n                    icon: Icon(\n                      Icons.lock_outlined,\n                      color: Theme.of(context).colorScheme.error,\n                    )),\n          ),\n          TextButton(\n              onPressed: _favEditController.onSubmit, child: const Text('保存')),\n          const SizedBox(width: 14),\n        ],\n      ),\n      body: Form(\n        key: _favEditController.formKey, //设置globalKey，用于后面获取FormState\n        autovalidateMode: AutovalidateMode.disabled,\n        child: Column(\n          children: [\n            Container(\n              padding: const EdgeInsets.fromLTRB(14, 10, 14, 5),\n              decoration: BoxDecoration(\n                border: Border(\n                  bottom: BorderSide(\n                    color: Theme.of(context).dividerColor.withOpacity(0.2),\n                  ),\n                ),\n              ),\n              child: TextFormField(\n                autofocus: true,\n                controller: _favEditController.titleController,\n                focusNode: _favEditController.titleTextFieldNode,\n                decoration: const InputDecoration(\n                  hintText: \"收藏夹名称\",\n                  enabledBorder: InputBorder.none,\n                  focusedBorder: InputBorder.none,\n                ),\n                // 校验标题\n                validator: (v) {\n                  return v!.trim().isNotEmpty ? null : \"请输入收藏夹名称\";\n                },\n                onChanged: (val) {\n                  _favEditController.title = val;\n                },\n              ),\n            ),\n            Expanded(\n              child: Container(\n                  padding:\n                      const EdgeInsets.symmetric(horizontal: 14, vertical: 5),\n                  child: TextFormField(\n                    controller: _favEditController.contentController,\n                    minLines: 1,\n                    maxLines: 5,\n                    decoration: const InputDecoration(\n                        hintText: '输入收藏夹简介', border: InputBorder.none),\n                    style: Theme.of(context).textTheme.bodyLarge,\n                    onChanged: (val) {\n                      _favEditController.intro = val;\n                    },\n                  )),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav_search/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/user/fav_detail.dart';\n\nimport '../../http/video.dart';\n\nclass FavSearchController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  Rx<TextEditingController> controller = TextEditingController().obs;\n  final FocusNode searchFocusNode = FocusNode();\n  RxString searchKeyWord = ''.obs; // 搜索词\n  String hintText = '请输入已收藏视频名称'; // 默认\n  RxBool loadingStatus = false.obs; // 加载状态\n  RxString loadingText = '加载中...'.obs; // 加载提示\n  bool hasMore = false;\n  late int searchType;\n  late int mediaId;\n\n  int currentPage = 1; // 当前页\n  int count = 0; // 总数\n  RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    searchType = int.parse(Get.parameters['searchType']!);\n    mediaId = int.parse(Get.parameters['mediaId']!);\n  }\n\n  // 清空搜索\n  void onClear() {\n    if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {\n      controller.value.clear();\n      searchKeyWord.value = '';\n    } else {\n      Get.back();\n    }\n  }\n\n  void onChange(value) {\n    searchKeyWord.value = value;\n  }\n\n  //  提交搜索内容\n  void submit() {\n    loadingStatus.value = true;\n    currentPage = 1;\n    searchFav();\n  }\n\n  // 搜索收藏夹视频\n  Future searchFav({type = 'init'}) async {\n    var res = await await UserHttp.userFavFolderDetail(\n      pn: currentPage,\n      ps: 20,\n      mediaId: mediaId,\n      keyword: searchKeyWord.value,\n      type: searchType,\n    );\n    if (res['status']) {\n      if (currentPage == 1 && type == 'init') {\n        favList.value = res['data'].medias;\n      } else if (type == 'onLoad') {\n        favList.addAll(res['data'].medias);\n      }\n      hasMore = res['data'].hasMore;\n    }\n    currentPage += 1;\n    loadingStatus.value = false;\n  }\n\n  onLoad() {\n    if (!hasMore) return;\n    searchFav(type: 'onLoad');\n  }\n\n  onCancelFav(int id) async {\n    var result = await VideoHttp.favVideo(\n        aid: id, addIds: '', delIds: mediaId.toString());\n    if (result['status']) {\n      List dataList = favList;\n      for (var i in dataList) {\n        if (i.id == id) {\n          dataList.remove(i);\n          break;\n        }\n      }\n      SmartDialog.showToast('取消收藏');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/fav_search/index.dart",
    "content": "library fav_search;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/fav_search/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/pages/fav_detail/widget/fav_video_card.dart';\n\nimport 'controller.dart';\n\nclass FavSearchPage extends StatefulWidget {\n  const FavSearchPage({super.key});\n\n  @override\n  State<FavSearchPage> createState() => _FavSearchPageState();\n}\n\nclass _FavSearchPageState extends State<FavSearchPage> {\n  final FavSearchController _favSearchCtr = Get.put(FavSearchController());\n  late ScrollController scrollController;\n  late int searchType;\n\n  @override\n  void initState() {\n    super.initState();\n    searchType = int.parse(Get.parameters['searchType']!);\n    scrollController = _favSearchCtr.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('fav', const Duration(seconds: 1), () {\n            _favSearchCtr.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        actions: [\n          IconButton(\n              onPressed: () => _favSearchCtr.submit(),\n              icon: const Icon(Icons.search_outlined, size: 22)),\n          const SizedBox(width: 10)\n        ],\n        title: Obx(\n          () => TextField(\n            autofocus: true,\n            focusNode: _favSearchCtr.searchFocusNode,\n            controller: _favSearchCtr.controller.value,\n            textInputAction: TextInputAction.search,\n            onChanged: (value) => _favSearchCtr.onChange(value),\n            decoration: InputDecoration(\n              hintText: _favSearchCtr.hintText,\n              border: InputBorder.none,\n              suffixIcon: IconButton(\n                icon: Icon(\n                  Icons.clear,\n                  size: 22,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n                onPressed: () => _favSearchCtr.onClear(),\n              ),\n            ),\n            onSubmitted: (String value) => _favSearchCtr.submit(),\n          ),\n        ),\n      ),\n      body: Obx(\n        () => _favSearchCtr.loadingStatus.value && _favSearchCtr.favList.isEmpty\n            ? ListView.builder(\n                itemCount: 10,\n                itemBuilder: (context, index) {\n                  return const VideoCardHSkeleton();\n                },\n              )\n            : _favSearchCtr.favList.isNotEmpty\n                ? ListView.builder(\n                    controller: scrollController,\n                    itemCount: _favSearchCtr.favList.length + 1,\n                    itemBuilder: (context, index) {\n                      if (index == _favSearchCtr.favList.length) {\n                        return Container(\n                          height: MediaQuery.of(context).padding.bottom + 60,\n                          padding: EdgeInsets.only(\n                              bottom: MediaQuery.of(context).padding.bottom),\n                        );\n                      } else {\n                        return FavVideoCardH(\n                          videoItem: _favSearchCtr.favList[index],\n                          searchType: searchType,\n                          isOwner: '0',\n                          callFn: () => searchType != 1\n                              ? _favSearchCtr\n                                  .onCancelFav(_favSearchCtr.favList[index].id!)\n                              : {},\n                        );\n                      }\n                    },\n                  )\n                : const CustomScrollView(\n                    slivers: <Widget>[\n                      NoData(),\n                    ],\n                  ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/follow/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/follow.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/follow/result.dart';\nimport 'package:pilipala/models/member/tags.dart';\nimport 'package:pilipala/utils/storage.dart';\n\n/// 查看自己的关注时，可以查看分类\n/// 查看其他人的关注时，只可以看全部\nclass FollowController extends GetxController with GetTickerProviderStateMixin {\n  Box userInfoCache = GStrorage.userInfo;\n  int pn = 1;\n  int ps = 20;\n  int total = 0;\n  RxList<FollowItemModel> followList = <FollowItemModel>[].obs;\n  late int mid;\n  late String name;\n  var userInfo;\n  RxString loadingText = '加载中...'.obs;\n  RxBool isOwner = false.obs;\n  late List<MemberTagItemModel> followTags;\n  late TabController tabController;\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n    mid = Get.parameters['mid'] != null\n        ? int.parse(Get.parameters['mid']!)\n        : userInfo.mid;\n    isOwner.value = mid == userInfo.mid;\n    name = Get.parameters['name'] ?? userInfo.uname;\n  }\n\n  Future queryFollowings(type) async {\n    if (type == 'init') {\n      pn = 1;\n      loadingText.value == '加载中...';\n    }\n    if (loadingText.value == '没有更多了') {\n      return;\n    }\n    var res = await FollowHttp.followings(\n      vmid: mid,\n      pn: pn,\n      ps: ps,\n      orderType: 'attention',\n    );\n    if (res['status']) {\n      if (type == 'init') {\n        followList.value = res['data'].list;\n        total = res['data'].total;\n      } else if (type == 'onLoad') {\n        followList.addAll(res['data'].list);\n      }\n      if ((pn == 1 && total < ps) || res['data'].list.isEmpty) {\n        loadingText.value = '没有更多了';\n      }\n      pn += 1;\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n    return res;\n  }\n\n  // 当查看当前用户的关注时，请求关注分组\n  Future followUpTags() async {\n    if (userInfo != null && mid == userInfo.mid) {\n      var res = await MemberHttp.followUpTags();\n      if (res['status']) {\n        followTags = res['data'];\n        tabController = TabController(\n          initialIndex: 0,\n          length: res['data'].length,\n          vsync: this,\n        );\n      }\n      return res;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/follow/index.dart",
    "content": "library following;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/follow/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'controller.dart';\nimport 'widgets/follow_list.dart';\nimport 'widgets/owner_follow_list.dart';\n\nclass FollowPage extends StatefulWidget {\n  const FollowPage({super.key});\n\n  @override\n  State<FollowPage> createState() => _FollowPageState();\n}\n\nclass _FollowPageState extends State<FollowPage> {\n  late String mid;\n  late FollowController _followController;\n  final ScrollController scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    mid = Get.parameters['mid']!;\n    _followController = Get.put(FollowController(), tag: mid);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        elevation: 0,\n        scrolledUnderElevation: 0,\n        titleSpacing: 0,\n        centerTitle: false,\n        title: Text(\n          _followController.isOwner.value\n              ? '我的关注'\n              : '${_followController.name}的关注',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n        actions: [\n          IconButton(\n            onPressed: () => Get.toNamed('/followSearch?mid=$mid'),\n            icon: const Icon(Icons.search_outlined),\n          ),\n          PopupMenuButton(\n            icon: const Icon(Icons.more_vert),\n            itemBuilder: (BuildContext context) => <PopupMenuEntry>[\n              PopupMenuItem(\n                onTap: () => Get.toNamed('/blackListPage'),\n                child: const Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(Icons.block, size: 19),\n                    SizedBox(width: 10),\n                    Text('黑名单管理'),\n                  ],\n                ),\n              )\n            ],\n          ),\n          const SizedBox(width: 6),\n        ],\n      ),\n      body: Obx(\n        () => !_followController.isOwner.value\n            ? FollowList(ctr: _followController)\n            : FutureBuilder(\n                future: _followController.followUpTags(),\n                builder: (context, snapshot) {\n                  if (snapshot.connectionState == ConnectionState.done) {\n                    var data = snapshot.data;\n                    if (data['status']) {\n                      return Column(\n                        children: [\n                          TabBar(\n                              controller: _followController.tabController,\n                              isScrollable: true,\n                              tabAlignment: TabAlignment.start,\n                              tabs: [\n                                for (var i in data['data']) ...[\n                                  Tab(text: i.name),\n                                ]\n                              ]),\n                          Expanded(\n                            child: TabBarView(\n                              controller: _followController.tabController,\n                              children: [\n                                for (var i = 0;\n                                    i < _followController.tabController.length;\n                                    i++) ...[\n                                  OwnerFollowList(\n                                    ctr: _followController,\n                                    tagItem: _followController.followTags[i],\n                                  )\n                                ]\n                              ],\n                            ),\n                          ),\n                        ],\n                      );\n                    } else {\n                      return const SizedBox();\n                    }\n                  } else {\n                    return const SizedBox();\n                  }\n                },\n              ),\n      ),\n    );\n  }\n}\n\nclass _FakeAPI {\n  static const List<String> _kOptions = <String>[\n    'aardvark',\n    'bobcat',\n    'chameleon',\n  ];\n  // Searches the options, but injects a fake \"network\" delay.\n  static Future<Iterable<String>> search(String query) async {\n    await Future<void>.delayed(\n        const Duration(seconds: 1)); // Fake 1 second delay.\n    if (query == '') {\n      return const Iterable<String>.empty();\n    }\n    return _kOptions.where((String option) {\n      return option.contains(query.toLowerCase());\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/follow/widgets/follow_item.dart",
    "content": "import 'package:bottom_sheet/bottom_sheet.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/follow/result.dart';\nimport 'package:pilipala/pages/follow/index.dart';\nimport 'package:pilipala/pages/video/detail/introduction/widgets/group_panel.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass FollowItem extends StatelessWidget {\n  final FollowItemModel item;\n  final FollowController? ctr;\n  const FollowItem({super.key, required this.item, this.ctr});\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(item.mid);\n    return ListTile(\n      onTap: () {\n        feedBack();\n        Get.toNamed('/member?mid=${item.mid}',\n            arguments: {'face': item.face, 'heroTag': heroTag});\n      },\n      leading: Hero(\n        tag: heroTag,\n        child: NetworkImgLayer(\n          width: 45,\n          height: 45,\n          type: 'avatar',\n          src: item.face,\n        ),\n      ),\n      title: Text(\n        item.uname!,\n        maxLines: 1,\n        overflow: TextOverflow.ellipsis,\n        style: const TextStyle(fontSize: 14),\n      ),\n      subtitle: Text(\n        item.sign!,\n        maxLines: 1,\n        overflow: TextOverflow.ellipsis,\n      ),\n      dense: true,\n      trailing: ctr != null && ctr!.isOwner.value\n          ? SizedBox(\n              height: 34,\n              child: TextButton(\n                onPressed: () async {\n                  await showFlexibleBottomSheet(\n                    bottomSheetBorderRadius: const BorderRadius.only(\n                      topLeft: Radius.circular(16),\n                      topRight: Radius.circular(16),\n                    ),\n                    minHeight: 1,\n                    initHeight: 1,\n                    maxHeight: 1,\n                    context: Get.context!,\n                    builder: (BuildContext context,\n                        ScrollController scrollController, double offset) {\n                      return GroupPanel(\n                        mid: item.mid!,\n                        scrollController: scrollController,\n                      );\n                    },\n                    anchors: [1],\n                    isSafeArea: true,\n                  );\n                },\n                style: TextButton.styleFrom(\n                  padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),\n                  foregroundColor: Theme.of(context).colorScheme.outline,\n                  backgroundColor:\n                      Theme.of(context).colorScheme.onInverseSurface, // 设置按钮背景色\n                ),\n                child: const Text(\n                  '已关注',\n                  style: TextStyle(fontSize: 12),\n                ),\n              ),\n            )\n          : const SizedBox(),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/follow/widgets/follow_list.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/models/follow/result.dart';\nimport 'package:pilipala/pages/follow/index.dart';\n\nimport 'follow_item.dart';\n\nclass FollowList extends StatefulWidget {\n  final FollowController ctr;\n  const FollowList({\n    super.key,\n    required this.ctr,\n  });\n\n  @override\n  State<FollowList> createState() => _FollowListState();\n}\n\nclass _FollowListState extends State<FollowList> {\n  late Future _futureBuilderFuture;\n  final ScrollController scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = widget.ctr.queryFollowings('init');\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('follow', const Duration(seconds: 1), () {\n            widget.ctr.queryFollowings('onLoad');\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return RefreshIndicator(\n      onRefresh: () async => await widget.ctr.queryFollowings('init'),\n      child: FutureBuilder(\n        future: _futureBuilderFuture,\n        builder: (context, snapshot) {\n          if (snapshot.connectionState == ConnectionState.done) {\n            var data = snapshot.data;\n            if (data['status']) {\n              List<FollowItemModel> list = widget.ctr.followList;\n              return Obx(\n                () => list.isNotEmpty\n                    ? ListView.builder(\n                        controller: scrollController,\n                        itemCount: list.length + 1,\n                        itemBuilder: (BuildContext context, int index) {\n                          if (index == list.length) {\n                            return Container(\n                              height:\n                                  MediaQuery.of(context).padding.bottom + 60,\n                              padding: EdgeInsets.only(\n                                  bottom:\n                                      MediaQuery.of(context).padding.bottom),\n                              child: Center(\n                                child: Obx(\n                                  () => Text(\n                                    widget.ctr.loadingText.value,\n                                    style: TextStyle(\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .outline,\n                                        fontSize: 13),\n                                  ),\n                                ),\n                              ),\n                            );\n                          } else {\n                            return FollowItem(\n                              item: list[index],\n                              ctr: widget.ctr,\n                            );\n                          }\n                        },\n                      )\n                    : const CustomScrollView(slivers: [NoData()]),\n              );\n            } else {\n              return CustomScrollView(\n                slivers: [\n                  HttpError(\n                    errMsg: data['msg'],\n                    fn: () => widget.ctr.queryFollowings('init'),\n                  )\n                ],\n              );\n            }\n          } else {\n            // 骨架屏\n            return const SizedBox();\n          }\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/follow/widgets/owner_follow_list.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/follow/result.dart';\nimport 'package:pilipala/models/member/tags.dart';\nimport 'package:pilipala/pages/follow/index.dart';\nimport 'follow_item.dart';\n\nclass OwnerFollowList extends StatefulWidget {\n  final FollowController ctr;\n  final MemberTagItemModel? tagItem;\n  const OwnerFollowList({super.key, required this.ctr, this.tagItem});\n\n  @override\n  State<OwnerFollowList> createState() => _OwnerFollowListState();\n}\n\nclass _OwnerFollowListState extends State<OwnerFollowList>\n    with AutomaticKeepAliveClientMixin {\n  late int mid;\n  late Future _futureBuilderFuture;\n  final ScrollController scrollController = ScrollController();\n  int pn = 1;\n  int ps = 20;\n  late MemberTagItemModel tagItem;\n  RxList<FollowItemModel> followList = <FollowItemModel>[].obs;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    mid = widget.ctr.mid;\n    tagItem = widget.tagItem!;\n    _futureBuilderFuture = followUpGroup('init');\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('follow', const Duration(seconds: 1), () {\n            followUpGroup('onLoad');\n          });\n        }\n      },\n    );\n  }\n\n  // 获取分组下up\n  Future followUpGroup(type) async {\n    if (type == 'init') {\n      pn = 1;\n    }\n    var res = await MemberHttp.followUpGroup(mid, tagItem.tagid, pn, ps);\n    if (res['status']) {\n      if (res['data'].isNotEmpty) {\n        if (type == 'init') {\n          followList.value = res['data'];\n        } else {\n          followList.addAll(res['data']);\n        }\n        pn += 1;\n      }\n    }\n    return res;\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return RefreshIndicator(\n      onRefresh: () async => await followUpGroup('init'),\n      child: FutureBuilder(\n        future: _futureBuilderFuture,\n        builder: (context, snapshot) {\n          if (snapshot.connectionState == ConnectionState.done) {\n            var data = snapshot.data;\n            if (data['status']) {\n              return Obx(\n                () => followList.isNotEmpty\n                    ? ListView.builder(\n                        physics: const AlwaysScrollableScrollPhysics(),\n                        controller: scrollController,\n                        itemCount: followList.length + 1,\n                        itemBuilder: (BuildContext context, int index) {\n                          if (index == followList.length) {\n                            return Container(\n                              height:\n                                  MediaQuery.of(context).padding.bottom + 60,\n                              padding: EdgeInsets.only(\n                                  bottom:\n                                      MediaQuery.of(context).padding.bottom),\n                            );\n                          } else {\n                            return FollowItem(\n                              item: followList[index],\n                              ctr: widget.ctr,\n                            );\n                          }\n                        },\n                      )\n                    : const CustomScrollView(slivers: [NoData()]),\n              );\n            } else {\n              return CustomScrollView(\n                slivers: [\n                  HttpError(\n                    errMsg: data['msg'],\n                    fn: () => widget.ctr.queryFollowings('init'),\n                  )\n                ],\n              );\n            }\n          } else {\n            // 骨架屏\n            return const SizedBox();\n          }\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/follow_search/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/member.dart';\n\nimport '../../models/follow/result.dart';\n\nclass FollowSearchController extends GetxController {\n  Rx<TextEditingController> controller = TextEditingController().obs;\n  final FocusNode searchFocusNode = FocusNode();\n  RxString searchKeyWord = ''.obs;\n  String hintText = '搜索';\n  RxString loadingStatus = 'init'.obs;\n  late int mid = 1;\n  RxString uname = ''.obs;\n  int ps = 20;\n  int pn = 1;\n  RxList<FollowItemModel> followList = <FollowItemModel>[].obs;\n  RxInt total = 0.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    mid = int.parse(Get.parameters['mid']!);\n  }\n\n  // 清空搜索\n  void onClear() {\n    if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {\n      controller.value.clear();\n      searchKeyWord.value = '';\n    } else {\n      Get.back();\n    }\n  }\n\n  void onChange(value) {\n    searchKeyWord.value = value;\n  }\n\n  //  提交搜索内容\n  void submit() {\n    loadingStatus.value = 'loading';\n    searchFollow();\n  }\n\n  Future searchFollow({type = 'init'}) async {\n    if (controller.value.text == '') {\n      return {'status': true, 'data': <FollowItemModel>[].obs};\n    }\n    if (type == 'init') {\n      pn = 1;\n    }\n    var res = await MemberHttp.getfollowSearch(\n      mid: mid,\n      ps: ps,\n      pn: pn,\n      name: controller.value.text,\n    );\n    if (res['status']) {\n      if (type == 'init') {\n        followList.value = res['data'].list;\n      } else {\n        followList.addAll(res['data'].list);\n      }\n      total.value = res['data'].total;\n    }\n    return res;\n  }\n\n  void onLoad() {\n    searchFollow(type: 'onLoad');\n  }\n}\n"
  },
  {
    "path": "lib/pages/follow_search/index.dart",
    "content": "library follow_search;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/follow_search/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/pages/follow_search/index.dart';\n\nimport '../follow/widgets/follow_item.dart';\n\nclass FollowSearchPage extends StatefulWidget {\n  const FollowSearchPage({super.key});\n\n  @override\n  State<FollowSearchPage> createState() => _FollowSearchPageState();\n}\n\nclass _FollowSearchPageState extends State<FollowSearchPage> {\n  final FollowSearchController _followSearchController =\n      Get.put(FollowSearchController());\n  late Future? _futureBuilder;\n  final ScrollController scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilder = _followSearchController.searchFollow();\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle(\n              'my-throttler', const Duration(milliseconds: 500), () {\n            _followSearchController.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  void reRequest() {\n    setState(() {\n      _futureBuilder = _followSearchController.searchFollow();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        actions: [\n          IconButton(\n            onPressed: reRequest,\n            icon: const Icon(CupertinoIcons.search, size: 22),\n          ),\n          const SizedBox(width: 6),\n        ],\n        title: TextField(\n          autofocus: true,\n          focusNode: _followSearchController.searchFocusNode,\n          controller: _followSearchController.controller.value,\n          textInputAction: TextInputAction.search,\n          onChanged: (value) => _followSearchController.onChange(value),\n          decoration: InputDecoration(\n            hintText: _followSearchController.hintText,\n            border: InputBorder.none,\n            suffixIcon: IconButton(\n              icon: Icon(\n                Icons.clear,\n                size: 22,\n                color: Theme.of(context).colorScheme.outline,\n              ),\n              onPressed: () => _followSearchController.onClear(),\n            ),\n          ),\n          onSubmitted: (String value) => reRequest(),\n        ),\n      ),\n      body: FutureBuilder(\n          future: _futureBuilder,\n          builder: (BuildContext context, AsyncSnapshot snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              var data = snapshot.data;\n              if (data == null) {\n                return CustomScrollView(\n                  slivers: [\n                    HttpError(errMsg: snapshot.data['msg'], fn: reRequest)\n                  ],\n                );\n              }\n              if (data['status']) {\n                RxList followList = _followSearchController.followList;\n                return Obx(\n                  () => followList.isNotEmpty\n                      ? ListView.builder(\n                          controller: scrollController,\n                          itemCount: followList.length,\n                          itemBuilder: ((context, index) {\n                            return FollowItem(\n                              item: followList[index],\n                            );\n                          }),\n                        )\n                      : CustomScrollView(\n                          slivers: [HttpError(errMsg: '未搜索到结果', fn: reRequest)],\n                        ),\n                );\n              } else {\n                return CustomScrollView(\n                  slivers: [\n                    HttpError(errMsg: snapshot.data['msg'], fn: reRequest)\n                  ],\n                );\n              }\n            } else {\n              return const SizedBox();\n            }\n          }),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/history/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/user/history.dart';\nimport 'package:pilipala/models/user/info.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass HistoryController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  RxList<HisListItem> historyList = <HisListItem>[].obs;\n  RxBool isLoadingMore = false.obs;\n  RxBool pauseStatus = false.obs;\n  Box localCache = GStrorage.localCache;\n  RxBool isLoading = false.obs;\n  RxBool enableMultiple = false.obs;\n  RxInt checkedCount = 0.obs;\n  Box userInfoCache = GStrorage.userInfo;\n  UserInfoData? userInfo;\n\n  @override\n  void onInit() {\n    super.onInit();\n    historyStatus();\n    userInfo = userInfoCache.get('userInfoCache');\n  }\n\n  Future queryHistoryList({type = 'init'}) async {\n    if (userInfo == null) {\n      return {'status': false, 'msg': '账号未登录', 'code': -101};\n    }\n    int max = 0;\n    int viewAt = 0;\n    if (type == 'onload') {\n      max = historyList.last.history!.oid!;\n      viewAt = historyList.last.viewAt!;\n    }\n    isLoadingMore.value = true;\n    var res = await UserHttp.historyList(max, viewAt);\n    isLoadingMore.value = false;\n    if (res['status']) {\n      if (type == 'onload') {\n        historyList.addAll(res['data'].list);\n      } else {\n        historyList.value = res['data'].list;\n      }\n    }\n    return res;\n  }\n\n  Future onLoad() async {\n    queryHistoryList(type: 'onload');\n  }\n\n  Future onRefresh() async {\n    queryHistoryList(type: 'onRefresh');\n  }\n\n  // 暂停观看历史\n  Future onPauseHistory() async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content:\n              Text(!pauseStatus.value ? '啊叻？你要暂停历史记录功能吗？' : '啊叻？要恢复历史记录功能吗？'),\n          actions: [\n            TextButton(\n                onPressed: () => SmartDialog.dismiss(),\n                child: const Text('取消')),\n            TextButton(\n              onPressed: () async {\n                SmartDialog.showLoading(msg: '请求中');\n                var res = await UserHttp.pauseHistory(!pauseStatus.value);\n                SmartDialog.dismiss();\n                if (res.data['code'] == 0) {\n                  SmartDialog.showToast(\n                      !pauseStatus.value ? '暂停观看历史' : '恢复观看历史');\n                  pauseStatus.value = !pauseStatus.value;\n                  localCache.put(LocalCacheKey.historyPause, pauseStatus.value);\n                }\n                SmartDialog.dismiss();\n              },\n              child: Text(!pauseStatus.value ? '确认暂停' : '确认恢复'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  // 观看历史暂停状态\n  Future historyStatus() async {\n    var res = await UserHttp.historyStatus();\n    if (res.data['code'] == 0) {\n      pauseStatus.value = res.data['data'];\n      localCache.put(LocalCacheKey.historyPause, res.data['data']);\n    }\n  }\n\n  // 清空观看历史\n  Future onClearHistory() async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: const Text('啊叻？你要清空历史记录功能吗？'),\n          actions: [\n            TextButton(\n                onPressed: () => SmartDialog.dismiss(),\n                child: const Text('取消')),\n            TextButton(\n              onPressed: () async {\n                SmartDialog.showLoading(msg: '请求中');\n                var res = await UserHttp.clearHistory();\n                SmartDialog.dismiss();\n                if (res.data['code'] == 0) {\n                  SmartDialog.showToast('清空观看历史');\n                }\n                SmartDialog.dismiss();\n                historyList.clear();\n              },\n              child: const Text('确认清空'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  // 删除某条历史记录\n  Future delHistory(kid, business) async {\n    String resKid = 'archive_$kid';\n    if (business == 'live') {\n      resKid = 'live_$kid';\n    } else if (business.contains('article')) {\n      resKid = 'article_$kid';\n    }\n\n    var res = await UserHttp.delHistory(resKid);\n    if (res['status']) {\n      historyList.removeWhere((e) => e.kid == kid);\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  // 删除已看历史记录\n  Future onDelHistory() async {\n    /// TODO 优化\n    List<HisListItem> result =\n        historyList.where((e) => e.progress == -1).toList();\n    for (HisListItem i in result) {\n      String resKid = 'archive_${i.kid}';\n      await UserHttp.delHistory(resKid);\n      historyList.removeWhere((e) => e.kid == i.kid);\n    }\n    SmartDialog.showToast('操作完成');\n  }\n\n  // 删除选中的记录\n  Future onDelCheckedHistory() async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: const Text('确认删除所选历史记录吗？'),\n          actions: [\n            TextButton(\n              onPressed: () => SmartDialog.dismiss(),\n              child: Text(\n                '取消',\n                style: TextStyle(\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                /// TODO 优化\n                await SmartDialog.dismiss();\n                SmartDialog.showLoading(msg: '请求中');\n                List<HisListItem> result =\n                    historyList.where((e) => e.checked!).toList();\n                for (HisListItem i in result) {\n                  String str = 'archive';\n                  try {\n                    str = i.history!.business!;\n                  } catch (_) {}\n                  String resKid = '${str}_${i.kid}';\n                  await UserHttp.delHistory(resKid);\n                  historyList.removeWhere((e) => e.kid == i.kid);\n                }\n                checkedCount.value = 0;\n                SmartDialog.dismiss();\n                enableMultiple.value = false;\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/history/index.dart",
    "content": "library history;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/history/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/pages/history/index.dart';\nimport 'package:pilipala/utils/route_push.dart';\n\nimport 'widgets/item.dart';\n\nclass HistoryPage extends StatefulWidget {\n  const HistoryPage({super.key});\n\n  @override\n  State<HistoryPage> createState() => _HistoryPageState();\n}\n\nclass _HistoryPageState extends State<HistoryPage> {\n  final HistoryController _historyController = Get.put(HistoryController());\n  Future? _futureBuilderFuture;\n  late ScrollController scrollController;\n\n  @override\n  void initState() {\n    _futureBuilderFuture = _historyController.queryHistoryList();\n    super.initState();\n    scrollController = _historyController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          if (!_historyController.isLoadingMore.value) {\n            EasyThrottle.throttle('history', const Duration(seconds: 1), () {\n              _historyController.onLoad();\n            });\n          }\n        }\n      },\n    );\n    _historyController.enableMultiple.listen((p0) {\n      setState(() {});\n    });\n  }\n\n  // 选中\n  onChoose(index) {\n    _historyController.historyList[index].checked =\n        !_historyController.historyList[index].checked!;\n    _historyController.checkedCount.value =\n        _historyController.historyList.where((item) => item.checked!).length;\n    _historyController.historyList.refresh();\n  }\n\n  // 更新多选状态\n  onUpdateMultiple() {\n    setState(() {});\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBarWidget(\n        visible: _historyController.enableMultiple.value,\n        child1: AppBar(\n          titleSpacing: 0,\n          centerTitle: false,\n          title: Text(\n            '观看记录',\n            style: Theme.of(context).textTheme.titleMedium,\n          ),\n          actions: [\n            IconButton(\n              onPressed: () => Get.toNamed('/historySearch'),\n              icon: const Icon(Icons.search_outlined),\n            ),\n            PopupMenuButton<String>(\n              onSelected: (String type) {\n                // 处理菜单项选择的逻辑\n                switch (type) {\n                  case 'pause':\n                    _historyController.onPauseHistory();\n                    break;\n                  case 'clear':\n                    _historyController.onClearHistory();\n                    break;\n                  case 'del':\n                    _historyController.onDelHistory();\n                    break;\n                  case 'multiple':\n                    _historyController.enableMultiple.value = true;\n                    setState(() {});\n                    break;\n                  default:\n                }\n              },\n              itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[\n                PopupMenuItem<String>(\n                  value: 'pause',\n                  child: Obx(\n                    () => Text(!_historyController.pauseStatus.value\n                        ? '暂停观看记录'\n                        : '恢复观看记录'),\n                  ),\n                ),\n                const PopupMenuItem<String>(\n                  value: 'clear',\n                  child: Text('清空观看记录'),\n                ),\n                const PopupMenuItem<String>(\n                  value: 'del',\n                  child: Text('删除已看记录'),\n                ),\n                const PopupMenuItem<String>(\n                  value: 'multiple',\n                  child: Text('多选删除'),\n                ),\n              ],\n            ),\n            const SizedBox(width: 6),\n          ],\n        ),\n        child2: AppBar(\n          titleSpacing: 0,\n          centerTitle: false,\n          leading: IconButton(\n            onPressed: () {\n              _historyController.enableMultiple.value = false;\n              for (var item in _historyController.historyList) {\n                item.checked = false;\n              }\n              _historyController.checkedCount.value = 0;\n              setState(() {});\n            },\n            icon: const Icon(Icons.close_outlined),\n          ),\n          title: Obx(\n            () => Text(\n              '已选择${_historyController.checkedCount.value}项',\n              style: Theme.of(context).textTheme.titleMedium,\n            ),\n          ),\n          actions: [\n            TextButton(\n              onPressed: () {\n                for (var item in _historyController.historyList) {\n                  item.checked = true;\n                }\n                _historyController.checkedCount.value =\n                    _historyController.historyList.length;\n                _historyController.historyList.refresh();\n              },\n              child: const Text('全选'),\n            ),\n            TextButton(\n              onPressed: () => _historyController.onDelCheckedHistory(),\n              child: Text(\n                '删除',\n                style: TextStyle(color: Theme.of(context).colorScheme.error),\n              ),\n            ),\n            const SizedBox(width: 6),\n          ],\n        ),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async {\n          await _historyController.onRefresh();\n          return;\n        },\n        child: CustomScrollView(\n          controller: _historyController.scrollController,\n          slivers: [\n            FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  if (snapshot.data == null) {\n                    return const SliverToBoxAdapter(child: SizedBox());\n                  }\n                  Map? data = snapshot.data;\n                  if (data != null && data['status']) {\n                    return Obx(\n                      () => _historyController.historyList.isNotEmpty\n                          ? SliverList(\n                              delegate: SliverChildBuilderDelegate(\n                                  (context, index) {\n                                return HistoryItem(\n                                  videoItem:\n                                      _historyController.historyList[index],\n                                  ctr: _historyController,\n                                  onChoose: () => onChoose(index),\n                                  onUpdateMultiple: () => onUpdateMultiple(),\n                                );\n                              },\n                                  childCount:\n                                      _historyController.historyList.length),\n                            )\n                          : _historyController.isLoadingMore.value\n                              ? const SliverToBoxAdapter(\n                                  child: Center(child: Text('加载中')),\n                                )\n                              : const NoData(),\n                    );\n                  } else {\n                    return HttpError(\n                      errMsg: data?['msg'] ?? '请求异常',\n                      btnText: data?['code'] == -101 ? '去登录' : null,\n                      fn: () {\n                        if (data?['code'] == -101) {\n                          RoutePush.loginRedirectPush();\n                        } else {\n                          setState(() {\n                            _futureBuilderFuture =\n                                _historyController.queryHistoryList();\n                          });\n                        }\n                      },\n                    );\n                  }\n                } else {\n                  // 骨架屏\n                  return SliverList(\n                    delegate: SliverChildBuilderDelegate((context, index) {\n                      return const VideoCardHSkeleton();\n                    }, childCount: 10),\n                  );\n                }\n              },\n            ),\n            SliverToBoxAdapter(\n              child: SizedBox(\n                height: MediaQuery.of(context).padding.bottom + 10,\n              ),\n            )\n          ],\n        ),\n      ),\n      // bottomNavigationBar: BottomAppBar(),\n    );\n  }\n}\n\nclass AppBarWidget extends StatelessWidget implements PreferredSizeWidget {\n  const AppBarWidget({\n    required this.child1,\n    required this.child2,\n    required this.visible,\n    Key? key,\n  }) : super(key: key);\n\n  final PreferredSizeWidget child1;\n  final PreferredSizeWidget child2;\n  final bool visible;\n  @override\n  Size get preferredSize => child1.preferredSize;\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedSwitcher(\n      duration: const Duration(milliseconds: 500),\n      transitionBuilder: (Widget child, Animation<double> animation) {\n        return ScaleTransition(\n          scale: animation,\n          child: child,\n        );\n      },\n      child: !visible ? child1 : child2,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/history/widgets/item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/common/business_type.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/models/live/item.dart';\nimport 'package:pilipala/pages/history_search/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass HistoryItem extends StatelessWidget {\n  final dynamic videoItem;\n  final dynamic ctr;\n  final Function? onChoose;\n  final Function? onUpdateMultiple;\n  const HistoryItem({\n    super.key,\n    required this.videoItem,\n    this.ctr,\n    this.onChoose,\n    this.onUpdateMultiple,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    int aid = videoItem.history.oid;\n    String bvid = videoItem.history.bvid ?? IdUtils.av2bv(aid);\n    String heroTag = Utils.makeHeroTag(aid);\n    return InkWell(\n      onTap: () async {\n        if (ctr!.enableMultiple.value) {\n          feedBack();\n          onChoose!();\n          return;\n        }\n        if (videoItem.history.business.contains('article')) {\n          int cid = videoItem.history.cid ??\n              videoItem.history.oid ??\n              await SearchHttp.ab2c(aid: aid, bvid: bvid);\n          if (cid == -1) {\n            return SmartDialog.showToast('无法获取文章内容');\n          }\n          Get.toNamed(\n            '/read',\n            parameters: {\n              'title': videoItem.title,\n              'id': cid.toString(),\n              'articleType': 'read',\n            },\n          );\n        } else if (videoItem.history.business == 'live') {\n          if (videoItem.liveStatus == 1) {\n            LiveItemModel liveItem = LiveItemModel.fromJson({\n              'face': videoItem.authorFace,\n              'roomid': videoItem.history.oid,\n              'pic': videoItem.cover,\n              'title': videoItem.title,\n              'uname': videoItem.authorName,\n              'cover': videoItem.cover,\n            });\n            Get.toNamed(\n              '/liveRoom?roomid=${videoItem.history.oid}',\n              arguments: {'liveItem': liveItem},\n            );\n          } else {\n            SmartDialog.showToast('直播未开播');\n          }\n        } else if (videoItem.badge == '番剧' ||\n            videoItem.tagName.contains('动画')) {\n          /// hack\n          var bvid = videoItem.history.bvid;\n          if (bvid != null && bvid != '') {\n            var result = await VideoHttp.videoIntro(bvid: bvid);\n            if (result['status']) {\n              String bvid = result['data'].bvid!;\n              int cid = result['data'].cid!;\n              String pic = result['data'].pic!;\n              String heroTag = Utils.makeHeroTag(cid);\n              var epid = result['data'].epId;\n              if (epid != null) {\n                Get.toNamed(\n                  '/video?bvid=$bvid&cid=$cid&epId=${result['data'].epId}',\n                  arguments: {\n                    'pic': pic,\n                    'heroTag': heroTag,\n                    'videoType': SearchType.media_bangumi,\n                  },\n                );\n              } else {\n                int cid = videoItem.history.cid ??\n                    // videoItem.history.oid ??\n                    await SearchHttp.ab2c(aid: aid, bvid: bvid);\n                Get.toNamed('/video?bvid=$bvid&cid=$cid',\n                    arguments: {'heroTag': heroTag, 'pic': videoItem.cover});\n              }\n            }\n          } else {\n            if (videoItem.history.epid != '') {\n              RoutePush.bangumiPush(\n                null,\n                videoItem.history.epid,\n                heroTag: heroTag,\n              );\n            }\n          }\n        } else {\n          int cid = videoItem.history.cid ??\n              // videoItem.history.oid ??\n              await SearchHttp.ab2c(aid: aid, bvid: bvid);\n          Get.toNamed('/video?bvid=$bvid&cid=$cid',\n              arguments: {'heroTag': heroTag, 'pic': videoItem.cover});\n        }\n      },\n      onLongPress: () {\n        if (ctr is HistorySearchController) {\n          return;\n        }\n        if (!ctr!.enableMultiple.value) {\n          feedBack();\n          ctr!.enableMultiple.value = true;\n          onChoose!();\n          onUpdateMultiple!();\n        }\n      },\n      child: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.fromLTRB(\n                StyleString.safeSpace, 5, StyleString.safeSpace, 5),\n            child: LayoutBuilder(\n              builder: (context, boxConstraints) {\n                double width =\n                    (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;\n                return SizedBox(\n                  height: width / StyleString.aspectRatio,\n                  child: Row(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Stack(\n                        children: [\n                          AspectRatio(\n                            aspectRatio: StyleString.aspectRatio,\n                            child: LayoutBuilder(\n                              builder: (context, boxConstraints) {\n                                double maxWidth = boxConstraints.maxWidth;\n                                double maxHeight = boxConstraints.maxHeight;\n                                return Stack(\n                                  children: [\n                                    Hero(\n                                      tag: heroTag,\n                                      child: NetworkImgLayer(\n                                        src: (videoItem.cover != ''\n                                            ? videoItem.cover\n                                            : videoItem.covers.first),\n                                        width: maxWidth,\n                                        height: maxHeight,\n                                      ),\n                                    ),\n                                    if (!BusinessType\n                                        .hiddenDurationType.hiddenDurationType\n                                        .contains(videoItem.history.business))\n                                      PBadge(\n                                        text: videoItem.progress == -1\n                                            ? '已看完'\n                                            : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',\n                                        right: 6.0,\n                                        bottom: 8.0,\n                                        type: 'gray',\n                                      ),\n                                    // 右上角\n                                    if (BusinessType.showBadge.showBadge\n                                            .contains(\n                                                videoItem.history.business) ||\n                                        videoItem.history.business ==\n                                            BusinessType.live.type)\n                                      PBadge(\n                                        text: videoItem.badge,\n                                        top: 6.0,\n                                        right: 6.0,\n                                        bottom: null,\n                                        left: null,\n                                      ),\n                                  ],\n                                );\n                              },\n                            ),\n                          ),\n                          Obx(\n                            () => Positioned.fill(\n                              child: AnimatedOpacity(\n                                opacity: ctr!.enableMultiple.value ? 1 : 0,\n                                duration: const Duration(milliseconds: 200),\n                                child: Container(\n                                  decoration: BoxDecoration(\n                                    borderRadius: BorderRadius.circular(\n                                        StyleString.imgRadius.x),\n                                    color: Colors.black.withOpacity(\n                                        ctr!.enableMultiple.value &&\n                                                videoItem.checked\n                                            ? 0.6\n                                            : 0),\n                                  ),\n                                  child: Center(\n                                    child: SizedBox(\n                                      width: 34,\n                                      height: 34,\n                                      child: AnimatedScale(\n                                        scale: videoItem.checked ? 1 : 0,\n                                        duration:\n                                            const Duration(milliseconds: 250),\n                                        curve: Curves.easeInOut,\n                                        child: IconButton(\n                                          style: ButtonStyle(\n                                            padding: MaterialStateProperty.all(\n                                                EdgeInsets.zero),\n                                            backgroundColor:\n                                                MaterialStateProperty\n                                                    .resolveWith(\n                                              (states) {\n                                                return Colors.white\n                                                    .withOpacity(0.8);\n                                              },\n                                            ),\n                                          ),\n                                          onPressed: () {\n                                            feedBack();\n                                            onChoose!();\n                                          },\n                                          icon: Icon(Icons.done_all_outlined,\n                                              color: Theme.of(context)\n                                                  .colorScheme\n                                                  .primary),\n                                        ),\n                                      ),\n                                    ),\n                                  ),\n                                ),\n                              ),\n                            ),\n                          ),\n                          videoItem.progress != 0 && videoItem.duration != 0\n                              ? Positioned(\n                                  left: 3,\n                                  right: 3,\n                                  bottom: 0,\n                                  child: ClipRRect(\n                                    borderRadius: BorderRadius.only(\n                                      bottomLeft: Radius.circular(\n                                          StyleString.imgRadius.x),\n                                      bottomRight: Radius.circular(\n                                          StyleString.imgRadius.x),\n                                    ),\n                                    child: LinearProgressIndicator(\n                                      value: videoItem.progress == -1\n                                          ? 100\n                                          : videoItem.progress /\n                                              videoItem.duration,\n                                    ),\n                                  ),\n                                )\n                              : const SizedBox()\n                        ],\n                      ),\n                      VideoContent(videoItem: videoItem, ctr: ctr)\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass VideoContent extends StatelessWidget {\n  final dynamic videoItem;\n  final dynamic ctr;\n  const VideoContent({super.key, required this.videoItem, this.ctr});\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              videoItem.title,\n              textAlign: TextAlign.start,\n              style: const TextStyle(\n                fontWeight: FontWeight.w500,\n                letterSpacing: 0.3,\n              ),\n              maxLines: videoItem.videos > 1 ? 1 : 2,\n              overflow: TextOverflow.ellipsis,\n            ),\n            if (videoItem.showTitle != null) ...[\n              const SizedBox(height: 2),\n              Text(\n                videoItem.showTitle,\n                textAlign: TextAlign.start,\n                style: TextStyle(\n                    fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                    fontWeight: FontWeight.w400,\n                    color: Theme.of(context).colorScheme.outline),\n                maxLines: 1,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ],\n            const Spacer(),\n            if (videoItem.authorName != '')\n              Row(\n                children: [\n                  Text(\n                    videoItem.authorName,\n                    style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                  ),\n                ],\n              ),\n            Row(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                Text(\n                  Utils.dateFormat(videoItem.viewAt!),\n                  style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline),\n                ),\n                SizedBox(\n                  width: 24,\n                  height: 24,\n                  child: PopupMenuButton<String>(\n                    padding: EdgeInsets.zero,\n                    tooltip: '功能菜单',\n                    icon: Icon(\n                      Icons.more_vert_outlined,\n                      color: Theme.of(context).colorScheme.outline,\n                      size: 14,\n                    ),\n                    position: PopupMenuPosition.under,\n                    // constraints: const BoxConstraints(maxHeight: 35),\n                    onSelected: (String type) {},\n                    itemBuilder: (BuildContext context) =>\n                        <PopupMenuEntry<String>>[\n                      if (videoItem.badge != '番剧' &&\n                          !videoItem.tagName.contains('动画') &&\n                          videoItem.history.business != 'live' &&\n                          !videoItem.history.business.contains('article'))\n                        PopupMenuItem<String>(\n                          onTap: () async {\n                            var res = await UserHttp.toViewLater(\n                                bvid: videoItem.history.bvid);\n                            SmartDialog.showToast(res['msg']);\n                          },\n                          value: 'pause',\n                          height: 35,\n                          child: const Row(\n                            children: [\n                              Icon(Icons.watch_later_outlined, size: 16),\n                              SizedBox(width: 6),\n                              Text('稍后再看', style: TextStyle(fontSize: 13))\n                            ],\n                          ),\n                        ),\n                      PopupMenuItem<String>(\n                        onTap: () => ctr!.delHistory(\n                            videoItem.kid, videoItem.history.business),\n                        value: 'pause',\n                        height: 35,\n                        child: const Row(\n                          children: [\n                            Icon(Icons.close_outlined, size: 16),\n                            SizedBox(width: 6),\n                            Text('删除记录', style: TextStyle(fontSize: 13))\n                          ],\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/history_search/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/user/history.dart';\n\nclass HistorySearchController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  Rx<TextEditingController> controller = TextEditingController().obs;\n  final FocusNode searchFocusNode = FocusNode();\n  RxString searchKeyWord = ''.obs;\n  String hintText = '搜索';\n  RxBool loadingStatus = false.obs;\n  RxString loadingText = '加载中...'.obs;\n  late int mid;\n  RxString uname = ''.obs;\n  int pn = 1;\n  int count = 0;\n  RxList<HisListItem> historyList = <HisListItem>[].obs;\n  RxBool enableMultiple = false.obs;\n\n  // 清空搜索\n  void onClear() {\n    if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {\n      controller.value.clear();\n      searchKeyWord.value = '';\n    } else {\n      Get.back();\n    }\n  }\n\n  void onChange(value) {\n    searchKeyWord.value = value;\n  }\n\n  //  提交搜索内容\n  void submit() {\n    if (!loadingStatus.value) {\n      pn = 1;\n      searchHistories();\n    }\n  }\n\n  // 搜索视频\n  Future searchHistories({type = 'init'}) async {\n    if (type == 'onLoad' && loadingText.value == '没有更多了') {\n      return;\n    }\n    loadingStatus.value = true;\n    var res = await UserHttp.searchHistory(\n      pn: pn,\n      keyword: controller.value.text,\n    );\n    if (res['status']) {\n      if (type == 'init' && pn == 1) {\n        historyList.value = res['data'].list;\n      } else {\n        historyList.addAll(res['data'].list);\n      }\n      count = res['data'].page['total'];\n      if (historyList.length == count) {\n        loadingText.value = '没有更多了';\n      }\n      pn += 1;\n    }\n    loadingStatus.value = false;\n    return res;\n  }\n\n  onLoad() {\n    searchHistories(type: 'onLoad');\n  }\n\n  Future delHistory(kid, business) async {\n    String resKid = 'archive_$kid';\n    if (business == 'live') {\n      resKid = 'live_$kid';\n    } else if (business.contains('article')) {\n      resKid = 'article_$kid';\n    }\n\n    var res = await UserHttp.delHistory(resKid);\n    if (res['status']) {\n      historyList.removeWhere((e) => e.kid == kid);\n      SmartDialog.showToast(res['msg']);\n    }\n    // loadingStatus.value = fasle;\n  }\n}\n"
  },
  {
    "path": "lib/pages/history_search/index.dart",
    "content": "library history_search;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/history_search/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/pages/history/widgets/item.dart';\n\nimport 'controller.dart';\n\nclass HistorySearchPage extends StatefulWidget {\n  const HistorySearchPage({super.key});\n\n  @override\n  State<HistorySearchPage> createState() => _HistorySearchPageState();\n}\n\nclass _HistorySearchPageState extends State<HistorySearchPage> {\n  final HistorySearchController _hisCtr = Get.put(HistorySearchController());\n  late ScrollController scrollController;\n\n  @override\n  void initState() {\n    super.initState();\n    scrollController = _hisCtr.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('history', const Duration(seconds: 1), () {\n            _hisCtr.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        actions: [\n          IconButton(\n              onPressed: () => _hisCtr.submit(),\n              icon: const Icon(Icons.search_outlined, size: 22)),\n          const SizedBox(width: 10)\n        ],\n        title: Obx(\n          () => TextField(\n            autofocus: true,\n            focusNode: _hisCtr.searchFocusNode,\n            controller: _hisCtr.controller.value,\n            textInputAction: TextInputAction.search,\n            onChanged: (value) => _hisCtr.onChange(value),\n            decoration: InputDecoration(\n              hintText: _hisCtr.hintText,\n              border: InputBorder.none,\n              suffixIcon: IconButton(\n                icon: Icon(\n                  Icons.clear,\n                  size: 22,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n                onPressed: () => _hisCtr.onClear(),\n              ),\n            ),\n            onSubmitted: (String value) => _hisCtr.submit(),\n          ),\n        ),\n      ),\n      body: Obx(\n        () {\n          return _hisCtr.loadingStatus.value && _hisCtr.historyList.isEmpty\n              ? ListView.builder(\n                  itemCount: 10,\n                  itemBuilder: (context, index) {\n                    return const VideoCardHSkeleton();\n                  },\n                )\n              : _hisCtr.historyList.isNotEmpty\n                  ? ListView.builder(\n                      controller: scrollController,\n                      itemCount: _hisCtr.historyList.length + 1,\n                      itemBuilder: (context, index) {\n                        if (index == _hisCtr.historyList.length) {\n                          return Container(\n                            height: MediaQuery.of(context).padding.bottom + 60,\n                            padding: EdgeInsets.only(\n                                bottom: MediaQuery.of(context).padding.bottom),\n                            child: Center(\n                              child: Obx(\n                                () => Text(\n                                  _hisCtr.loadingText.value,\n                                  style: TextStyle(\n                                    color:\n                                        Theme.of(context).colorScheme.outline,\n                                    fontSize: 13,\n                                  ),\n                                ),\n                              ),\n                            ),\n                          );\n                        } else {\n                          return HistoryItem(\n                            videoItem: _hisCtr.historyList[index],\n                            ctr: _hisCtr,\n                            onChoose: null,\n                            onUpdateMultiple: () => null,\n                          );\n                        }\n                      },\n                    )\n                  : const CustomScrollView(\n                      slivers: <Widget>[\n                        NoData(),\n                      ],\n                    );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/home/controller.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/tab_type.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport '../../http/index.dart';\n\nclass HomeController extends GetxController with GetTickerProviderStateMixin {\n  bool flag = false;\n  late RxList tabs = [].obs;\n  RxInt initialIndex = 1.obs;\n  late TabController tabController;\n  late List tabsCtrList;\n  late List<Widget> tabsPageList;\n  Box userInfoCache = GStrorage.userInfo;\n  Box settingStorage = GStrorage.setting;\n  RxBool userLogin = false.obs;\n  RxString userFace = ''.obs;\n  var userInfo;\n  Box setting = GStrorage.setting;\n  late final StreamController<bool> searchBarStream =\n      StreamController<bool>.broadcast();\n  late bool hideSearchBar;\n  late List defaultTabs;\n  late List<String> tabbarSort;\n  RxString defaultSearch = ''.obs;\n  late bool enableGradientBg;\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n    userLogin.value = userInfo != null;\n    userFace.value = userInfo != null ? userInfo.face : '';\n    hideSearchBar =\n        setting.get(SettingBoxKey.hideSearchBar, defaultValue: false);\n    if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {\n      searchDefault();\n    }\n    enableGradientBg =\n        setting.get(SettingBoxKey.enableGradientBg, defaultValue: true);\n    // 进行tabs配置\n    setTabConfig();\n  }\n\n  void onRefresh() {\n    int index = tabController.index;\n    var ctr = tabsCtrList[index];\n    ctr().onRefresh();\n  }\n\n  void animateToTop() {\n    int index = tabController.index;\n    var ctr = tabsCtrList[index];\n    ctr().animateToTop();\n  }\n\n  // 更新登录状态\n  void updateLoginStatus(val) async {\n    userInfo = await userInfoCache.get('userInfoCache');\n    userLogin.value = val ?? false;\n    if (val) return;\n    userFace.value = userInfo != null ? userInfo.face : '';\n  }\n\n  void setTabConfig() async {\n    defaultTabs = [...tabsConfig];\n    tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,\n        defaultValue: ['live', 'rcmd', 'hot', 'bangumi']);\n    defaultTabs.retainWhere(\n        (item) => tabbarSort.contains((item['type'] as TabType).id));\n    defaultTabs.sort((a, b) => tabbarSort\n        .indexOf((a['type'] as TabType).id)\n        .compareTo(tabbarSort.indexOf((b['type'] as TabType).id)));\n\n    tabs.value = defaultTabs;\n\n    if (tabbarSort.contains(TabType.rcmd.id)) {\n      initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id);\n    } else {\n      initialIndex.value = 0;\n    }\n    tabsCtrList = tabs.map((e) => e['ctr']).toList();\n    tabsPageList = tabs.map<Widget>((e) => e['page']).toList();\n\n    tabController = TabController(\n      initialIndex: initialIndex.value,\n      length: tabs.length,\n      vsync: this,\n    );\n    // 监听 tabController 切换\n    if (enableGradientBg) {\n      tabController.animation!.addListener(() {\n        if (tabController.indexIsChanging) {\n          if (initialIndex.value != tabController.index) {\n            initialIndex.value = tabController.index;\n          }\n        } else {\n          final int temp = tabController.animation!.value.round();\n          if (initialIndex.value != temp) {\n            initialIndex.value = temp;\n            tabController.index = initialIndex.value;\n          }\n        }\n      });\n    }\n  }\n\n  void searchDefault() async {\n    var res = await Request().get(Api.searchDefault);\n    if (res.data['code'] == 0) {\n      defaultSearch.value = res.data['data']['name'];\n    }\n  }\n\n  @override\n  void onClose() {\n    searchBarStream.close();\n    super.onClose();\n  }\n}\n"
  },
  {
    "path": "lib/pages/home/index.dart",
    "content": "library home;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/home/view.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/pages/mine/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport './controller.dart';\n\nclass HomePage extends StatefulWidget {\n  const HomePage({Key? key}) : super(key: key);\n\n  @override\n  State<HomePage> createState() => _HomePageState();\n}\n\nclass _HomePageState extends State<HomePage>\n    with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {\n  final HomeController _homeController = Get.put(HomeController());\n  List videoList = [];\n  late Stream<bool> stream;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    stream = _homeController.searchBarStream.stream;\n  }\n\n  showUserBottomSheet() {\n    feedBack();\n    showModalBottomSheet(\n      context: context,\n      builder: (_) => const SizedBox(\n        height: 450,\n        child: MinePage(),\n      ),\n      clipBehavior: Clip.hardEdge,\n      isScrollControlled: true,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return Scaffold(\n      extendBody: true,\n      extendBodyBehindAppBar: true,\n      backgroundColor: Colors.transparent,\n      appBar: AppBar(\n        toolbarHeight: 0,\n        elevation: 0,\n        backgroundColor: Colors.transparent,\n        systemOverlayStyle: Platform.isAndroid\n            ? SystemUiOverlayStyle(\n                statusBarIconBrightness:\n                    Theme.of(context).brightness == Brightness.dark\n                        ? Brightness.light\n                        : Brightness.dark,\n              )\n            : Theme.of(context).brightness == Brightness.dark\n                ? SystemUiOverlayStyle.light\n                : SystemUiOverlayStyle.dark,\n      ),\n      body: Column(\n        children: [\n          CustomAppBar(\n            stream: _homeController.hideSearchBar\n                ? stream\n                : StreamController<bool>.broadcast().stream,\n            ctr: _homeController,\n            callback: showUserBottomSheet,\n          ),\n          if (_homeController.tabs.length > 1) ...[\n            if (_homeController.enableGradientBg) ...[\n              const CustomTabs(),\n            ] else ...[\n              Container(\n                width: double.infinity,\n                height: 42,\n                padding: const EdgeInsets.only(top: 4),\n                child: Align(\n                  alignment: Alignment.center,\n                  child: TabBar(\n                    controller: _homeController.tabController,\n                    tabs: [\n                      for (var i in _homeController.tabs) Tab(text: i['label'])\n                    ],\n                    isScrollable: true,\n                    dividerColor: Colors.transparent,\n                    enableFeedback: true,\n                    splashBorderRadius: BorderRadius.circular(10),\n                    tabAlignment: TabAlignment.center,\n                    onTap: (value) {\n                      feedBack();\n                      if (_homeController.initialIndex.value == value) {\n                        _homeController.tabsCtrList[value]().animateToTop();\n                      }\n                      _homeController.initialIndex.value = value;\n                    },\n                  ),\n                ),\n              ),\n            ],\n          ] else ...[\n            const SizedBox(height: 6),\n          ],\n          Expanded(\n            child: TabBarView(\n              controller: _homeController.tabController,\n              children: _homeController.tabsPageList,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass CustomAppBar extends StatelessWidget implements PreferredSizeWidget {\n  final double height;\n  final Stream<bool>? stream;\n  final HomeController? ctr;\n  final Function? callback;\n\n  const CustomAppBar({\n    super.key,\n    this.height = kToolbarHeight,\n    this.stream,\n    this.ctr,\n    this.callback,\n  });\n\n  @override\n  Size get preferredSize => Size.fromHeight(height);\n\n  @override\n  Widget build(BuildContext context) {\n    return StreamBuilder(\n      stream: stream!.distinct(),\n      initialData: true,\n      builder: (BuildContext context, AsyncSnapshot snapshot) {\n        final RxBool isUserLoggedIn = ctr!.userLogin;\n        final double top = MediaQuery.of(context).padding.top;\n        return AnimatedOpacity(\n          opacity: snapshot.data ? 1 : 0,\n          duration: const Duration(milliseconds: 300),\n          child: AnimatedContainer(\n            curve: Curves.easeInOutCubicEmphasized,\n            duration: const Duration(milliseconds: 500),\n            height: snapshot.data ? top + 52 : top,\n            padding: EdgeInsets.fromLTRB(14, top + 6, 14, 0),\n            child: UserInfoWidget(\n              top: top,\n              ctr: ctr,\n              userLogin: isUserLoggedIn,\n              userFace: ctr?.userFace.value,\n              callback: () => callback!(),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass UserInfoWidget extends StatelessWidget {\n  const UserInfoWidget({\n    Key? key,\n    required this.top,\n    required this.userLogin,\n    required this.userFace,\n    required this.callback,\n    required this.ctr,\n  }) : super(key: key);\n\n  final double top;\n  final RxBool userLogin;\n  final String? userFace;\n  final VoidCallback? callback;\n  final HomeController? ctr;\n\n  Widget buildLoggedInWidget(context) {\n    return Stack(\n      children: [\n        NetworkImgLayer(\n          type: 'avatar',\n          width: 34,\n          height: 34,\n          src: userFace,\n        ),\n        Positioned.fill(\n          child: Material(\n            color: Colors.transparent,\n            child: InkWell(\n              onTap: () => callback?.call(),\n              splashColor: Theme.of(context)\n                  .colorScheme\n                  .primaryContainer\n                  .withOpacity(0.3),\n              borderRadius: const BorderRadius.all(\n                Radius.circular(50),\n              ),\n            ),\n          ),\n        )\n      ],\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        SearchBar(ctr: ctr),\n        if (userLogin.value) ...[\n          const SizedBox(width: 4),\n          ClipRect(\n            child: IconButton(\n              onPressed: () => Get.toNamed('/whisper'),\n              icon: const Icon(Icons.notifications_none),\n            ),\n          )\n        ],\n        const SizedBox(width: 8),\n        Obx(\n          () => userLogin.value\n              ? buildLoggedInWidget(context)\n              : DefaultUser(callback: () => callback!()),\n        ),\n      ],\n    );\n  }\n}\n\nclass DefaultUser extends StatelessWidget {\n  const DefaultUser({super.key, this.callback});\n  final Function? callback;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 38,\n      height: 38,\n      child: IconButton(\n        style: ButtonStyle(\n          padding: MaterialStateProperty.all(EdgeInsets.zero),\n          backgroundColor: MaterialStateProperty.resolveWith((states) {\n            return Theme.of(context)\n                .colorScheme\n                .onSecondaryContainer\n                .withOpacity(0.05);\n          }),\n        ),\n        onPressed: () => callback?.call(),\n        icon: Icon(\n          Icons.person_rounded,\n          size: 22,\n          color: Theme.of(context).colorScheme.primary,\n        ),\n      ),\n    );\n  }\n}\n\nclass CustomTabs extends StatefulWidget {\n  const CustomTabs({super.key});\n\n  @override\n  State<CustomTabs> createState() => _CustomTabsState();\n}\n\nclass _CustomTabsState extends State<CustomTabs> {\n  final HomeController _homeController = Get.put(HomeController());\n\n  void onTap(int index) {\n    feedBack();\n    if (_homeController.initialIndex.value == index) {\n      _homeController.tabsCtrList[index]().animateToTop();\n    }\n    _homeController.initialIndex.value = index;\n    _homeController.tabController.index = index;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 44,\n      margin: const EdgeInsets.only(top: 8),\n      child: Obx(\n        () => ListView.separated(\n          padding: const EdgeInsets.symmetric(horizontal: 14.0),\n          scrollDirection: Axis.horizontal,\n          itemCount: _homeController.tabs.length,\n          separatorBuilder: (BuildContext context, int index) {\n            return const SizedBox(width: 10);\n          },\n          itemBuilder: (BuildContext context, int index) {\n            String label = _homeController.tabs[index]['label'];\n            return Obx(\n              () => CustomChip(\n                onTap: () => onTap(index),\n                label: label,\n                selected: index == _homeController.initialIndex.value,\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass CustomChip extends StatelessWidget {\n  final Function onTap;\n  final String label;\n  final bool selected;\n  const CustomChip({\n    super.key,\n    required this.onTap,\n    required this.label,\n    required this.selected,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final ColorScheme colorTheme = Theme.of(context).colorScheme;\n    final Color secondaryContainer = colorTheme.secondaryContainer;\n    final Color onPrimary = colorTheme.onPrimary;\n    final Color primary = colorTheme.primary;\n    final TextStyle chipTextStyle = selected\n        ? TextStyle(fontSize: 13, color: onPrimary)\n        : TextStyle(fontSize: 13, color: colorTheme.onSecondaryContainer);\n    const VisualDensity visualDensity =\n        VisualDensity(horizontal: -4.0, vertical: -2.0);\n    return InputChip(\n      side: BorderSide.none,\n      backgroundColor: secondaryContainer,\n      color: MaterialStateProperty.resolveWith((states) {\n        if (states.contains(MaterialState.selected) ||\n            states.contains(MaterialState.hovered)) {\n          return primary;\n        }\n        return colorTheme.secondaryContainer;\n      }),\n      padding: const EdgeInsets.fromLTRB(6, 1, 6, 1),\n      label: Text(label, style: chipTextStyle),\n      onPressed: () => onTap(),\n      shape: RoundedRectangleBorder(\n        borderRadius: BorderRadius.circular(6),\n      ),\n      selected: selected,\n      showCheckmark: false,\n      visualDensity: visualDensity,\n    );\n  }\n}\n\nclass SearchBar extends StatelessWidget {\n  const SearchBar({\n    Key? key,\n    required this.ctr,\n  }) : super(key: key);\n\n  final HomeController? ctr;\n\n  @override\n  Widget build(BuildContext context) {\n    final ColorScheme colorScheme = Theme.of(context).colorScheme;\n    return Expanded(\n      child: Container(\n        width: 250,\n        height: 44,\n        clipBehavior: Clip.hardEdge,\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(25),\n        ),\n        child: Material(\n          color: colorScheme.onSecondaryContainer.withOpacity(0.05),\n          child: InkWell(\n            splashColor: colorScheme.primaryContainer.withOpacity(0.3),\n            onTap: () => Get.toNamed('/search',\n                parameters: {'hintText': ctr!.defaultSearch.value}),\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 14),\n              child: Row(\n                children: [\n                  Icon(\n                    Icons.search_outlined,\n                    color: colorScheme.onSecondaryContainer,\n                  ),\n                  const SizedBox(width: 10),\n                  Obx(\n                    () => Text(\n                      ctr!.defaultSearch.value,\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                      style: TextStyle(color: colorScheme.outline),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/home/widgets/app_bar.dart",
    "content": "import 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/pages/mine/view.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nBox userInfoCache = GStrorage.userInfo;\n\nclass HomeAppBar extends StatelessWidget {\n  const HomeAppBar({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    var userInfo = userInfoCache.get('userInfoCache');\n    return SliverAppBar(\n      // forceElevated: true,\n      scrolledUnderElevation: 0,\n      toolbarHeight: MediaQuery.of(context).padding.top,\n      expandedHeight: kToolbarHeight + MediaQuery.of(context).padding.top,\n      automaticallyImplyLeading: false,\n      pinned: true,\n      floating: true,\n      primary: false,\n      flexibleSpace: LayoutBuilder(\n        builder: (context, constraints) {\n          return FlexibleSpaceBar(\n            background: Column(\n              children: [\n                AppBar(\n                  centerTitle: false,\n                  title: const Text(\n                    'PiLiPaLa',\n                    style: TextStyle(\n                      fontSize: 20,\n                      fontWeight: FontWeight.bold,\n                      letterSpacing: 1,\n                      fontFamily: 'ArchivoNarrow',\n                    ),\n                  ),\n                  actions: [\n                    Hero(\n                      tag: 'searchTag',\n                      child: IconButton(\n                        onPressed: () {\n                          Get.toNamed('/search');\n                        },\n                        icon: const Icon(CupertinoIcons.search, size: 22),\n                      ),\n                    ),\n                    // IconButton(\n                    //   onPressed: () {},\n                    //   icon: const Icon(CupertinoIcons.bell, size: 22),\n                    // ),\n                    const SizedBox(width: 6),\n\n                    /// TODO\n                    if (userInfo != null) ...[\n                      GestureDetector(\n                        onTap: () => showModalBottomSheet(\n                          context: context,\n                          builder: (_) => const SizedBox(\n                            height: 450,\n                            child: MinePage(),\n                          ),\n                          clipBehavior: Clip.hardEdge,\n                          isScrollControlled: true,\n                        ),\n                        child: NetworkImgLayer(\n                          type: 'avatar',\n                          width: 32,\n                          height: 32,\n                          src: userInfo.face,\n                        ),\n                      ),\n                      const SizedBox(width: 10),\n                    ] else ...[\n                      IconButton(\n                        onPressed: () => showModalBottomSheet(\n                          context: context,\n                          builder: (_) => const SizedBox(\n                            height: 450,\n                            child: MinePage(),\n                          ),\n                          clipBehavior: Clip.hardEdge,\n                          isScrollControlled: true,\n                        ),\n                        icon: const Icon(CupertinoIcons.person, size: 22),\n                      ),\n                    ],\n\n                    const SizedBox(width: 10)\n                  ],\n                  elevation: 0,\n                  scrolledUnderElevation: 0,\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/hot/controller.dart",
    "content": "import 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/model_hot_video_item.dart';\n\nclass HotController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  final int _count = 20;\n  int _currentPage = 1;\n  RxList<HotVideoItemModel> videoList = <HotVideoItemModel>[].obs;\n  bool isLoadingMore = false;\n  bool flag = false;\n  OverlayEntry? popupDialog;\n\n  // 获取推荐\n  Future queryHotFeed(type) async {\n    var res = await VideoHttp.hotVideoList(\n      pn: _currentPage,\n      ps: _count,\n    );\n    if (res['status']) {\n      if (type == 'init') {\n        videoList.value = res['data'];\n      } else if (type == 'onRefresh') {\n        videoList.insertAll(0, res['data']);\n      } else if (type == 'onLoad') {\n        videoList.addAll(res['data']);\n      }\n      _currentPage += 1;\n    }\n    isLoadingMore = false;\n    return res;\n  }\n\n  // 下拉刷新\n  Future onRefresh() async {\n    queryHotFeed('onRefresh');\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    queryHotFeed('onLoad');\n  }\n\n  // 返回顶部并刷新\n  void animateToTop() async {\n    if (scrollController.offset >=\n        MediaQuery.of(Get.context!).size.height * 5) {\n      scrollController.jumpTo(0);\n    } else {\n      await scrollController.animateTo(0,\n          duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/hot/index.dart",
    "content": "library hot;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/hot/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/video_card_h.dart';\nimport 'package:pilipala/pages/hot/controller.dart';\nimport 'package:pilipala/utils/main_stream.dart';\n\nclass HotPage extends StatefulWidget {\n  const HotPage({Key? key}) : super(key: key);\n\n  @override\n  State<HotPage> createState() => _HotPageState();\n}\n\nclass _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {\n  final HotController _hotController = Get.put(HotController());\n  List videoList = [];\n  Future? _futureBuilderFuture;\n  late ScrollController scrollController;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _hotController.queryHotFeed('init');\n    scrollController = _hotController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          if (!_hotController.isLoadingMore) {\n            _hotController.isLoadingMore = true;\n            _hotController.onLoad();\n          }\n        }\n        handleScrollEvent(scrollController);\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return RefreshIndicator(\n      onRefresh: () async {\n        return await _hotController.onRefresh();\n      },\n      child: CustomScrollView(\n        controller: _hotController.scrollController,\n        slivers: [\n          SliverPadding(\n            // 单列布局 EdgeInsets.zero\n            padding:\n                const EdgeInsets.fromLTRB(0, StyleString.safeSpace - 5, 0, 0),\n            sliver: FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  Map data = snapshot.data as Map;\n                  if (data['status']) {\n                    return Obx(\n                      () => SliverList(\n                        delegate: SliverChildBuilderDelegate((context, index) {\n                          return VideoCardH(\n                            videoItem: _hotController.videoList[index],\n                            showPubdate: true,\n                          );\n                        }, childCount: _hotController.videoList.length),\n                      ),\n                    );\n                  } else {\n                    return HttpError(\n                      errMsg: data['msg'],\n                      fn: () {\n                        setState(() {\n                          _futureBuilderFuture =\n                              _hotController.queryHotFeed('init');\n                        });\n                      },\n                    );\n                  }\n                } else {\n                  // 骨架屏\n                  return SliverList(\n                    delegate: SliverChildBuilderDelegate((context, index) {\n                      return const VideoCardHSkeleton();\n                    }, childCount: 10),\n                  );\n                }\n              },\n            ),\n          ),\n          SliverToBoxAdapter(\n            child: SizedBox(\n              height: MediaQuery.of(context).padding.bottom + 10,\n            ),\n          )\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/html/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/html.dart';\nimport 'package:pilipala/http/reply.dart';\nimport 'package:pilipala/models/common/reply_sort_type.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass HtmlRenderController extends GetxController {\n  late String id;\n  late String dynamicType;\n  late int type;\n  RxInt oid = (-1).obs;\n  late Map response;\n  int? floor;\n  int currentPage = 0;\n  bool isLoadingMore = false;\n  RxString noMore = ''.obs;\n  RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;\n  RxInt acount = 0.obs;\n  final ScrollController scrollController = ScrollController();\n\n  ReplySortType _sortType = ReplySortType.time;\n  RxString sortTypeTitle = ReplySortType.time.titles.obs;\n  RxString sortTypeLabel = ReplySortType.time.labels.obs;\n  Box setting = GStrorage.setting;\n\n  @override\n  void onInit() {\n    super.onInit();\n    id = Get.parameters['id']!;\n    dynamicType = Get.parameters['dynamicType']!;\n    type = dynamicType == 'picture' ? 11 : 12;\n  }\n\n  // 请求动态内容\n  Future reqHtml(id) async {\n    late dynamic res;\n    if (dynamicType == 'opus' || dynamicType == 'picture') {\n      res = await HtmlHttp.reqHtml(id, dynamicType);\n    } else {\n      res = await HtmlHttp.reqReadHtml(id, dynamicType);\n    }\n    response = res;\n    oid.value = res['commentId'];\n    return res;\n  }\n\n  // 请求评论\n  Future queryReplyList({reqType = 'init'}) async {\n    var res = await ReplyHttp.replyList(\n      oid: oid.value,\n      pageNum: currentPage + 1,\n      type: type,\n      sort: _sortType.index,\n    );\n    if (res['status']) {\n      List<ReplyItemModel> replies = res['data'].replies;\n      acount.value = res['data'].page.acount;\n      if (replies.isNotEmpty) {\n        currentPage++;\n        noMore.value = '加载中...';\n        if (replies.length < 20) {\n          noMore.value = '没有更多了';\n        }\n      } else {\n        noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';\n      }\n      if (reqType == 'init') {\n        // 添加置顶回复\n        if (res['data'].upper.top != null) {\n          bool flag = res['data']\n              .topReplies\n              .any((reply) => reply.rpid == res['data'].upper.top.rpid);\n          if (!flag) {\n            replies.insert(0, res['data'].upper.top);\n          }\n        }\n        replies.insertAll(0, res['data'].topReplies);\n        replyList.value = replies;\n      } else {\n        replyList.addAll(replies);\n      }\n    }\n    isLoadingMore = false;\n    return res;\n  }\n\n  // 排序搜索评论\n  queryBySort() {\n    feedBack();\n    switch (_sortType) {\n      case ReplySortType.time:\n        _sortType = ReplySortType.like;\n        break;\n      case ReplySortType.like:\n        _sortType = ReplySortType.time;\n        break;\n      default:\n    }\n    sortTypeTitle.value = _sortType.titles;\n    sortTypeLabel.value = _sortType.labels;\n    currentPage = 0;\n    replyList.clear();\n    queryReplyList(reqType: 'init');\n  }\n}\n"
  },
  {
    "path": "lib/pages/html/index.dart",
    "content": "library html_render;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/html/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_reply.dart';\nimport 'package:pilipala/common/widgets/html_render.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';\nimport 'package:pilipala/pages/video/detail/reply_new/index.dart';\nimport 'package:pilipala/pages/video/detail/reply_reply/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\n\nimport 'controller.dart';\n\nclass HtmlRenderPage extends StatefulWidget {\n  const HtmlRenderPage({super.key});\n\n  @override\n  State<HtmlRenderPage> createState() => _HtmlRenderPageState();\n}\n\nclass _HtmlRenderPageState extends State<HtmlRenderPage>\n    with TickerProviderStateMixin {\n  final HtmlRenderController _htmlRenderCtr = Get.put(HtmlRenderController());\n  late String title;\n  late String id;\n  late String url;\n  late String dynamicType;\n  late int type;\n  bool _isFabVisible = true;\n  late Future _futureBuilderFuture;\n  late ScrollController scrollController;\n  late AnimationController fabAnimationCtr;\n\n  @override\n  void initState() {\n    super.initState();\n    title = Get.parameters['title']!;\n    id = Get.parameters['id']!;\n    url = Get.parameters['url']!;\n    dynamicType = Get.parameters['dynamicType']!;\n    type = dynamicType == 'picture' ? 11 : 12;\n    _futureBuilderFuture = _htmlRenderCtr.reqHtml(id);\n    fabAnimationCtr = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n    );\n    scrollListener();\n  }\n\n  void scrollListener() {\n    scrollController = _htmlRenderCtr.scrollController;\n    scrollController.addListener(\n      () {\n        // 分页加载\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {\n            _htmlRenderCtr.queryReplyList(reqType: 'onLoad');\n          });\n        }\n\n        // 标题\n        // if (scrollController.offset > 55 && !_visibleTitle) {\n        //   _visibleTitle = true;\n        //   titleStreamC.add(true);\n        // } else if (scrollController.offset <= 55 && _visibleTitle) {\n        //   _visibleTitle = false;\n        //   titleStreamC.add(false);\n        // }\n\n        // fab按钮\n        final ScrollDirection direction =\n            scrollController.position.userScrollDirection;\n        if (direction == ScrollDirection.forward) {\n          _showFab();\n        } else if (direction == ScrollDirection.reverse) {\n          _hideFab();\n        }\n      },\n    );\n  }\n\n  void _showFab() {\n    if (!_isFabVisible) {\n      _isFabVisible = true;\n      fabAnimationCtr.forward();\n    }\n  }\n\n  void _hideFab() {\n    if (_isFabVisible) {\n      _isFabVisible = false;\n      fabAnimationCtr.reverse();\n    }\n  }\n\n  void replyReply(replyItem) {\n    int oid = replyItem.oid;\n    int rpid = replyItem.rpid!;\n    Get.to(\n      () => Scaffold(\n        appBar: AppBar(\n          titleSpacing: 0,\n          centerTitle: false,\n          title: Text(\n            '评论详情',\n            style: Theme.of(context).textTheme.titleMedium,\n          ),\n        ),\n        body: VideoReplyReplyPanel(\n          oid: oid,\n          rpid: rpid,\n          source: 'dynamic',\n          replyType: ReplyType.values[type],\n          firstFloor: replyItem,\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          title,\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n        actions: [\n          const SizedBox(width: 4),\n          IconButton(\n            onPressed: () {\n              Get.toNamed('/webview', parameters: {\n                'url': url.startsWith('http') ? url : 'https:$url',\n                'type': 'url',\n                'pageTitle': title,\n              });\n            },\n            icon: const Icon(Icons.open_in_browser_outlined, size: 19),\n          ),\n          PopupMenuButton(\n            icon: const Icon(Icons.more_vert),\n            itemBuilder: (BuildContext context) => <PopupMenuEntry>[\n              PopupMenuItem(\n                onTap: () => {\n                  Clipboard.setData(ClipboardData(text: url)),\n                  SmartDialog.showToast('已复制'),\n                },\n                child: const Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(Icons.copy_rounded, size: 19),\n                    SizedBox(width: 10),\n                    Text('复制链接'),\n                  ],\n                ),\n              ),\n              PopupMenuItem(\n                onTap: () => {},\n                child: const Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(Icons.share_outlined, size: 19),\n                    SizedBox(width: 10),\n                    Text('分享'),\n                  ],\n                ),\n              ),\n            ],\n          ),\n          const SizedBox(width: 6)\n        ],\n      ),\n      body: Stack(\n        children: [\n          SingleChildScrollView(\n            controller: scrollController,\n            child: Column(\n              children: [\n                FutureBuilder(\n                  future: _futureBuilderFuture,\n                  builder: (context, snapshot) {\n                    if (snapshot.connectionState == ConnectionState.done) {\n                      var data = snapshot.data;\n                      fabAnimationCtr.forward();\n                      if (data['status']) {\n                        return Column(\n                          children: [\n                            Padding(\n                              padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),\n                              child: Row(\n                                children: [\n                                  NetworkImgLayer(\n                                    width: 40,\n                                    height: 40,\n                                    type: 'avatar',\n                                    src: _htmlRenderCtr.response['avatar']!,\n                                  ),\n                                  const SizedBox(width: 10),\n                                  Column(\n                                    crossAxisAlignment:\n                                        CrossAxisAlignment.start,\n                                    children: [\n                                      Text(_htmlRenderCtr.response['uname'],\n                                          style: TextStyle(\n                                            fontSize: Theme.of(context)\n                                                .textTheme\n                                                .titleSmall!\n                                                .fontSize,\n                                          )),\n                                      Text(\n                                        _htmlRenderCtr.response['updateTime'],\n                                        style: TextStyle(\n                                          color: Theme.of(context)\n                                              .colorScheme\n                                              .outline,\n                                          fontSize: Theme.of(context)\n                                              .textTheme\n                                              .labelSmall!\n                                              .fontSize,\n                                        ),\n                                      ),\n                                    ],\n                                  ),\n                                  const Spacer(),\n                                ],\n                              ),\n                            ),\n                            Padding(\n                              padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),\n                              child: HtmlRender(\n                                htmlContent: _htmlRenderCtr.response['content'],\n                              ),\n                            ),\n                            Container(\n                              decoration: BoxDecoration(\n                                border: Border(\n                                  bottom: BorderSide(\n                                    width: 8,\n                                    color: Theme.of(context)\n                                        .dividerColor\n                                        .withOpacity(0.05),\n                                  ),\n                                ),\n                              ),\n                            ),\n                          ],\n                        );\n                      } else {\n                        return const Text('error');\n                      }\n                    } else {\n                      // 骨架屏\n                      return const SizedBox();\n                    }\n                  },\n                ),\n                Obx(\n                  () => _htmlRenderCtr.oid.value != -1\n                      ? Container(\n                          decoration: BoxDecoration(\n                            color: Theme.of(context).colorScheme.surface,\n                            border: Border(\n                              top: BorderSide(\n                                width: 0.6,\n                                color: Theme.of(context)\n                                    .dividerColor\n                                    .withOpacity(0.05),\n                              ),\n                            ),\n                          ),\n                          height: 45,\n                          padding: const EdgeInsets.only(left: 12, right: 6),\n                          child: Row(\n                            children: [\n                              const Text('回复'),\n                              const Spacer(),\n                              SizedBox(\n                                height: 35,\n                                child: TextButton.icon(\n                                  onPressed: () => _htmlRenderCtr.queryBySort(),\n                                  icon: const Icon(Icons.sort, size: 16),\n                                  label: Obx(\n                                    () => Text(\n                                      _htmlRenderCtr.sortTypeLabel.value,\n                                      style: const TextStyle(fontSize: 13),\n                                    ),\n                                  ),\n                                ),\n                              )\n                            ],\n                          ),\n                        )\n                      : const SizedBox(),\n                ),\n                Obx(\n                  () => _htmlRenderCtr.oid.value != -1\n                      ? FutureBuilder(\n                          future: _htmlRenderCtr.queryReplyList(),\n                          builder: (context, snapshot) {\n                            if (snapshot.connectionState ==\n                                ConnectionState.done) {\n                              Map data = snapshot.data as Map;\n                              if (snapshot.data['status']) {\n                                // 请求成功\n                                return Obx(\n                                  () => _htmlRenderCtr.replyList.isEmpty &&\n                                          _htmlRenderCtr.isLoadingMore\n                                      ? ListView.builder(\n                                          itemCount: 5,\n                                          shrinkWrap: true,\n                                          physics:\n                                              const NeverScrollableScrollPhysics(),\n                                          itemBuilder: (context, index) {\n                                            return const VideoReplySkeleton();\n                                          },\n                                        )\n                                      : ListView.builder(\n                                          shrinkWrap: true,\n                                          physics:\n                                              const NeverScrollableScrollPhysics(),\n                                          itemCount:\n                                              _htmlRenderCtr.replyList.length +\n                                                  1,\n                                          itemBuilder: (context, index) {\n                                            if (index ==\n                                                _htmlRenderCtr\n                                                    .replyList.length) {\n                                              return Container(\n                                                padding: EdgeInsets.only(\n                                                    bottom:\n                                                        MediaQuery.of(context)\n                                                            .padding\n                                                            .bottom),\n                                                height: MediaQuery.of(context)\n                                                        .padding\n                                                        .bottom +\n                                                    100,\n                                                child: Center(\n                                                  child: Obx(\n                                                    () => Text(\n                                                      _htmlRenderCtr\n                                                          .noMore.value,\n                                                      style: TextStyle(\n                                                        fontSize: 12,\n                                                        color: Theme.of(context)\n                                                            .colorScheme\n                                                            .outline,\n                                                      ),\n                                                    ),\n                                                  ),\n                                                ),\n                                              );\n                                            } else {\n                                              return ReplyItem(\n                                                replyItem: _htmlRenderCtr\n                                                    .replyList[index],\n                                                showReplyRow: true,\n                                                replyLevel: '1',\n                                                replyReply: (replyItem) =>\n                                                    replyReply(replyItem),\n                                                replyType:\n                                                    ReplyType.values[type],\n                                                addReply: (replyItem) {\n                                                  _htmlRenderCtr\n                                                      .replyList[index].replies!\n                                                      .add(replyItem);\n                                                },\n                                              );\n                                            }\n                                          },\n                                        ),\n                                );\n                              } else {\n                                // 请求错误\n                                return CustomScrollView(\n                                  slivers: [\n                                    HttpError(\n                                      errMsg: data['msg'],\n                                      fn: () => setState(() {}),\n                                    )\n                                  ],\n                                );\n                              }\n                            } else {\n                              // 骨架屏\n                              return ListView.builder(\n                                shrinkWrap: true,\n                                physics: const NeverScrollableScrollPhysics(),\n                                itemCount: 5,\n                                itemBuilder: (context, index) {\n                                  return const VideoReplySkeleton();\n                                },\n                              );\n                            }\n                          },\n                        )\n                      : const SizedBox(),\n                )\n              ],\n            ),\n          ),\n          Positioned(\n            bottom: MediaQuery.of(context).padding.bottom + 14,\n            right: 14,\n            child: SlideTransition(\n              position: Tween<Offset>(\n                begin: const Offset(0, 2),\n                end: const Offset(0, 0),\n              ).animate(CurvedAnimation(\n                parent: fabAnimationCtr,\n                curve: Curves.easeInOut,\n              )),\n              child: FloatingActionButton(\n                heroTag: null,\n                onPressed: () {\n                  feedBack();\n                  showModalBottomSheet(\n                    context: context,\n                    isScrollControlled: true,\n                    builder: (BuildContext context) {\n                      return VideoReplyNewDialog(\n                        oid: _htmlRenderCtr.oid.value,\n                        root: 0,\n                        parent: 0,\n                        replyType: ReplyType.values[type],\n                      );\n                    },\n                  ).then(\n                    (value) => {\n                      // 完成评论，数据添加\n                      if (value != null && value['data'] != null)\n                        {\n                          _htmlRenderCtr.replyList.add(value['data']),\n                          _htmlRenderCtr.acount.value++\n                        }\n                    },\n                  );\n                },\n                tooltip: '评论动态',\n                child: const Icon(Icons.reply),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/later/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/model_hot_video_item.dart';\nimport 'package:pilipala/models/user/info.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass LaterController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs;\n  int count = 0;\n  RxBool isLoading = false.obs;\n  Box userInfoCache = GStrorage.userInfo;\n  UserInfoData? userInfo;\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n  }\n\n  Future queryLaterList() async {\n    if (userInfo == null) {\n      return {'status': false, 'msg': '账号未登录', 'code': -101};\n    }\n    isLoading.value = true;\n    var res = await UserHttp.seeYouLater();\n    if (res['status']) {\n      count = res['data']['count'];\n      if (count > 0) {\n        laterList.value = res['data']['list'];\n      }\n    }\n    isLoading.value = false;\n    return res;\n  }\n\n  Future toViewDel({int? aid}) async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: Text(\n              aid != null ? '即将移除该视频，确定是否移除' : '即将删除所有已观看视频，此操作不可恢复。确定是否删除？'),\n          actions: [\n            TextButton(\n              onPressed: SmartDialog.dismiss,\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                var res = await UserHttp.toViewDel(aid: aid);\n                if (res['status']) {\n                  if (aid != null) {\n                    laterList.removeWhere((e) => e.aid == aid);\n                  } else {\n                    laterList.clear();\n                    queryLaterList();\n                  }\n                }\n                SmartDialog.dismiss();\n                SmartDialog.showToast(res['msg']);\n              },\n              child: Text(aid != null ? '确认移除' : '确认删除'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  // 一键清空\n  Future toViewClear() async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('清空确认'),\n          content: const Text('确定要清空你的稍后再看列表吗？'),\n          actions: [\n            TextButton(\n              onPressed: SmartDialog.dismiss,\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                var res = await UserHttp.toViewClear();\n                if (res['status']) {\n                  laterList.clear();\n                }\n                SmartDialog.dismiss();\n                SmartDialog.showToast(res['msg']);\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  // 稍后再看播放全部\n  Future toViewPlayAll() async {\n    final HotVideoItemModel firstItem = laterList.first;\n    final String heroTag = Utils.makeHeroTag(firstItem.bvid);\n    Get.toNamed(\n      '/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',\n      arguments: {\n        'videoItem': firstItem,\n        'heroTag': heroTag,\n        'sourceType': 'watchLater',\n        'count': laterList.length,\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/later/index.dart",
    "content": "library later;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/later/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/common/widgets/video_card_h.dart';\nimport 'package:pilipala/pages/later/index.dart';\nimport 'package:pilipala/utils/route_push.dart';\n\nclass LaterPage extends StatefulWidget {\n  const LaterPage({super.key});\n\n  @override\n  State<LaterPage> createState() => _LaterPageState();\n}\n\nclass _LaterPageState extends State<LaterPage> {\n  final LaterController _laterController = Get.put(LaterController());\n  Future? _futureBuilderFuture;\n\n  @override\n  void initState() {\n    _futureBuilderFuture = _laterController.queryLaterList();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        centerTitle: false,\n        title: Obx(\n          () => _laterController.laterList.isNotEmpty\n              ? Text(\n                  '稍后再看 (${_laterController.laterList.length})',\n                  style: Theme.of(context).textTheme.titleMedium,\n                )\n              : Text(\n                  '稍后再看',\n                  style: Theme.of(context).textTheme.titleMedium,\n                ),\n        ),\n        actions: [\n          Obx(\n            () => _laterController.laterList.isNotEmpty\n                ? TextButton(\n                    onPressed: () => _laterController.toViewDel(),\n                    child: const Text('移除已看'),\n                  )\n                : const SizedBox(),\n          ),\n          Obx(\n            () => _laterController.laterList.isNotEmpty\n                ? IconButton(\n                    tooltip: '一键清空',\n                    onPressed: () => _laterController.toViewClear(),\n                    icon: Icon(\n                      Icons.clear_all_outlined,\n                      size: 21,\n                      color: Theme.of(context).colorScheme.primary,\n                    ),\n                  )\n                : const SizedBox(),\n          ),\n          const SizedBox(width: 8),\n        ],\n      ),\n      body: CustomScrollView(\n        controller: _laterController.scrollController,\n        slivers: [\n          FutureBuilder(\n            future: _futureBuilderFuture,\n            builder: (context, snapshot) {\n              if (snapshot.connectionState == ConnectionState.done) {\n                Map? data = snapshot.data;\n                if (data != null && data['status']) {\n                  return Obx(\n                    () => _laterController.laterList.isNotEmpty &&\n                            !_laterController.isLoading.value\n                        ? SliverList(\n                            delegate:\n                                SliverChildBuilderDelegate((context, index) {\n                              var videoItem = _laterController.laterList[index];\n                              return VideoCardH(\n                                  videoItem: videoItem,\n                                  source: 'later',\n                                  onPressedFn: () => _laterController.toViewDel(\n                                      aid: videoItem.aid));\n                            }, childCount: _laterController.laterList.length),\n                          )\n                        : _laterController.isLoading.value\n                            ? const SliverToBoxAdapter(\n                                child: Center(child: Text('加载中')),\n                              )\n                            : const NoData(),\n                  );\n                } else {\n                  return HttpError(\n                    errMsg: data?['msg'] ?? '请求异常',\n                    btnText: data?['code'] == -101 ? '去登录' : null,\n                    fn: () {\n                      if (data?['code'] == -101) {\n                        RoutePush.loginRedirectPush();\n                      } else {\n                        setState(() {\n                          _futureBuilderFuture =\n                              _laterController.queryLaterList();\n                        });\n                      }\n                    },\n                  );\n                }\n              } else {\n                // 骨架屏\n                return SliverList(\n                  delegate: SliverChildBuilderDelegate((context, index) {\n                    return const VideoCardHSkeleton();\n                  }, childCount: 10),\n                );\n              }\n            },\n          ),\n          SliverToBoxAdapter(\n            child: SizedBox(\n              height: MediaQuery.of(context).padding.bottom + 10,\n            ),\n          )\n        ],\n      ),\n      floatingActionButton: Obx(\n        () => _laterController.laterList.isNotEmpty\n            ? FloatingActionButton.extended(\n                onPressed: _laterController.toViewPlayAll,\n                label: const Text('播放全部'),\n                icon: const Icon(Icons.playlist_play),\n              )\n            : const SizedBox(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/live/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/live.dart';\nimport 'package:pilipala/models/live/follow.dart';\nimport 'package:pilipala/models/live/item.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass LiveController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  int count = 12;\n  int _currentPage = 1;\n  RxInt crossAxisCount = 2.obs;\n  RxList<LiveItemModel> liveList = <LiveItemModel>[].obs;\n  RxList<LiveFollowingItemModel> liveFollowingList =\n      <LiveFollowingItemModel>[].obs;\n  bool flag = false;\n  OverlayEntry? popupDialog;\n  Box setting = GStrorage.setting;\n\n  @override\n  void onInit() {\n    super.onInit();\n    crossAxisCount.value =\n        setting.get(SettingBoxKey.customRows, defaultValue: 2);\n  }\n\n  // 获取推荐\n  Future queryLiveList(type) async {\n    // if (type == 'init') {\n    //   _currentPage = 1;\n    // }\n    var res = await LiveHttp.liveList(\n      pn: _currentPage,\n    );\n    if (res['status']) {\n      if (type == 'init') {\n        liveList.value = res['data'];\n      } else if (type == 'onLoad') {\n        liveList.addAll(res['data']);\n      }\n      _currentPage += 1;\n    }\n    return res;\n  }\n\n  // 下拉刷新\n  Future onRefresh() async {\n    queryLiveList('init');\n    fetchLiveFollowing();\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    queryLiveList('onLoad');\n  }\n\n  // 返回顶部并刷新\n  void animateToTop() async {\n    if (scrollController.offset >=\n        MediaQuery.of(Get.context!).size.height * 5) {\n      scrollController.jumpTo(0);\n    } else {\n      await scrollController.animateTo(0,\n          duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);\n    }\n  }\n\n  //\n  Future fetchLiveFollowing() async {\n    var res = await LiveHttp.liveFollowing(pn: 1, ps: 20);\n    if (res['status']) {\n      liveFollowingList.value =\n          (res['data'].list as List<LiveFollowingItemModel>)\n              .where((LiveFollowingItemModel item) =>\n                  item.liveStatus == 1 && item.recordLiveTime == 0) // 根据条件过滤\n              .toList();\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/live/index.dart",
    "content": "library live;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/live/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/skeleton/video_card_v.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/live/follow.dart';\nimport 'package:pilipala/utils/main_stream.dart';\n\nimport 'controller.dart';\nimport 'widgets/live_item.dart';\n\nclass LivePage extends StatefulWidget {\n  const LivePage({super.key});\n\n  @override\n  State<LivePage> createState() => _LivePageState();\n}\n\nclass _LivePageState extends State<LivePage>\n    with AutomaticKeepAliveClientMixin {\n  final LiveController _liveController = Get.put(LiveController());\n  late Future _futureBuilderFuture;\n  late Future _futureBuilderFuture2;\n  late ScrollController scrollController;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _liveController.queryLiveList('init');\n    _futureBuilderFuture2 = _liveController.fetchLiveFollowing();\n    scrollController = _liveController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('liveList', const Duration(milliseconds: 200),\n              () {\n            _liveController.onLoad();\n          });\n        }\n        handleScrollEvent(scrollController);\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return Container(\n      clipBehavior: Clip.hardEdge,\n      margin: const EdgeInsets.only(\n          left: StyleString.safeSpace, right: StyleString.safeSpace),\n      decoration: const BoxDecoration(\n        borderRadius: BorderRadius.all(StyleString.imgRadius),\n      ),\n      child: RefreshIndicator(\n        onRefresh: () async {\n          return await _liveController.onRefresh();\n        },\n        child: CustomScrollView(\n          controller: _liveController.scrollController,\n          slivers: [\n            buildFollowingList(),\n            SliverPadding(\n              // 单列布局 EdgeInsets.zero\n              padding:\n                  const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),\n              sliver: FutureBuilder(\n                future: _futureBuilderFuture,\n                builder: (context, snapshot) {\n                  if (snapshot.connectionState == ConnectionState.done) {\n                    if (snapshot.data == null) {\n                      return const SliverToBoxAdapter(child: SizedBox());\n                    }\n                    Map data = snapshot.data as Map;\n                    if (data['status']) {\n                      return SliverLayoutBuilder(\n                          builder: (context, boxConstraints) {\n                        return Obx(() => contentGrid(\n                            _liveController, _liveController.liveList));\n                      });\n                    } else {\n                      return HttpError(\n                        errMsg: data['msg'],\n                        fn: () {\n                          setState(() {\n                            _futureBuilderFuture =\n                                _liveController.queryLiveList('init');\n                          });\n                        },\n                      );\n                    }\n                  } else {\n                    return contentGrid(_liveController, []);\n                  }\n                },\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget contentGrid(ctr, liveList) {\n    // double maxWidth = Get.size.width;\n    // int baseWidth = 500;\n    // int step = 300;\n    // int crossAxisCount =\n    //     maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;\n    // if (maxWidth < 300) {\n    //   crossAxisCount = 1;\n    // }\n    int crossAxisCount = ctr.crossAxisCount.value;\n    return SliverGrid(\n      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n        // 行间距\n        mainAxisSpacing: StyleString.safeSpace,\n        // 列间距\n        crossAxisSpacing: StyleString.safeSpace,\n        // 列数\n        crossAxisCount: crossAxisCount,\n        mainAxisExtent:\n            Get.size.width / crossAxisCount / StyleString.aspectRatio +\n                MediaQuery.textScalerOf(context).scale(\n                  (crossAxisCount == 1 ? 48 : 68),\n                ),\n      ),\n      delegate: SliverChildBuilderDelegate(\n        (BuildContext context, int index) {\n          return liveList!.isNotEmpty\n              ? LiveCardV(\n                  liveItem: liveList[index],\n                  crossAxisCount: crossAxisCount,\n                )\n              : const VideoCardVSkeleton();\n        },\n        childCount: liveList!.isNotEmpty ? liveList!.length : 10,\n      ),\n    );\n  }\n\n  // 关注的up直播\n  Widget buildFollowingList() {\n    return SliverPadding(\n      padding: const EdgeInsets.only(top: 16),\n      sliver: SliverToBoxAdapter(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Obx(\n              () => Text.rich(\n                TextSpan(\n                  children: [\n                    const TextSpan(\n                      text: ' 我的关注 ',\n                      style: TextStyle(\n                        fontWeight: FontWeight.bold,\n                        fontSize: 15,\n                      ),\n                    ),\n                    TextSpan(\n                      text: ' ${_liveController.liveFollowingList.length}',\n                      style: TextStyle(\n                        fontSize: 12,\n                        color: Theme.of(context).colorScheme.primary,\n                      ),\n                    ),\n                    TextSpan(\n                      text: '人正在直播',\n                      style: TextStyle(\n                        fontSize: 12,\n                        color: Theme.of(context).colorScheme.outline,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n            FutureBuilder(\n              future: _futureBuilderFuture2,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  if (snapshot.data == null) {\n                    return const SizedBox();\n                  }\n                  Map? data = snapshot.data;\n                  if (data?['status']) {\n                    RxList list = _liveController.liveFollowingList;\n                    // ignore: invalid_use_of_protected_member\n                    return Obx(() => LiveFollowingListView(list: list.value));\n                  } else {\n                    return SizedBox(\n                      height: 80,\n                      child: Center(\n                        child: Text(\n                          data?['msg'] ?? '',\n                          style: TextStyle(\n                            color: Theme.of(context).colorScheme.outline,\n                            fontSize: 12,\n                          ),\n                        ),\n                      ),\n                    );\n                  }\n                } else {\n                  return const SizedBox();\n                }\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass LiveFollowingListView extends StatelessWidget {\n  final List list;\n\n  const LiveFollowingListView({super.key, required this.list});\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 100,\n      child: ListView.builder(\n        scrollDirection: Axis.horizontal,\n        itemBuilder: (context, index) {\n          final LiveFollowingItemModel item = list[index];\n          return Padding(\n            padding: const EdgeInsets.fromLTRB(3, 12, 3, 0),\n            child: Column(\n              children: [\n                InkWell(\n                  onTap: () {\n                    Get.toNamed(\n                      '/liveRoom?roomid=${item.roomId}',\n                      arguments: {\n                        'liveItem': item,\n                        'heroTag': item.roomId.toString()\n                      },\n                    );\n                  },\n                  child: Container(\n                    width: 54,\n                    height: 54,\n                    padding: const EdgeInsets.all(2),\n                    decoration: BoxDecoration(\n                      borderRadius: BorderRadius.circular(27),\n                      border: Border.all(\n                        color: Theme.of(context).colorScheme.primary,\n                        width: 1.5,\n                      ),\n                    ),\n                    child: NetworkImgLayer(\n                      width: 50,\n                      height: 50,\n                      type: 'avatar',\n                      src: list[index].face,\n                    ),\n                  ),\n                ),\n                const SizedBox(height: 6),\n                SizedBox(\n                  width: 62,\n                  child: Text(\n                    list[index].uname,\n                    maxLines: 1,\n                    overflow: TextOverflow.ellipsis,\n                    textAlign: TextAlign.center,\n                    style: const TextStyle(\n                      fontSize: 12,\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          );\n        },\n        itemCount: list.length,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/live/widgets/live_item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/models/live/item.dart';\nimport 'package:pilipala/utils/image_save.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\n\n// 视频卡片 - 垂直布局\nclass LiveCardV extends StatelessWidget {\n  final LiveItemModel liveItem;\n  final int crossAxisCount;\n\n  const LiveCardV({\n    Key? key,\n    required this.liveItem,\n    required this.crossAxisCount,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(liveItem.roomId);\n    return InkWell(\n      onLongPress: () => imageSaveDialog(\n        context,\n        liveItem,\n        SmartDialog.dismiss,\n      ),\n      borderRadius: BorderRadius.circular(16),\n      onTap: () async {\n        Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',\n            arguments: {'liveItem': liveItem, 'heroTag': heroTag});\n      },\n      child: Column(\n        children: [\n          ClipRRect(\n            borderRadius: const BorderRadius.all(StyleString.imgRadius),\n            child: AspectRatio(\n              aspectRatio: StyleString.aspectRatio,\n              child: LayoutBuilder(builder: (context, boxConstraints) {\n                double maxWidth = boxConstraints.maxWidth;\n                double maxHeight = boxConstraints.maxHeight;\n                return Stack(\n                  children: [\n                    Hero(\n                      tag: heroTag,\n                      child: NetworkImgLayer(\n                        src: liveItem.cover!,\n                        width: maxWidth,\n                        height: maxHeight,\n                      ),\n                    ),\n                    if (crossAxisCount != 1)\n                      Positioned(\n                        left: 0,\n                        right: 0,\n                        bottom: 0,\n                        child: AnimatedOpacity(\n                          opacity: 1,\n                          duration: const Duration(milliseconds: 200),\n                          child: VideoStat(\n                            liveItem: liveItem,\n                          ),\n                        ),\n                      ),\n                  ],\n                );\n              }),\n            ),\n          ),\n          LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount)\n        ],\n      ),\n    );\n  }\n}\n\nclass LiveContent extends StatelessWidget {\n  final dynamic liveItem;\n  final int crossAxisCount;\n  const LiveContent(\n      {Key? key, required this.liveItem, required this.crossAxisCount})\n      : super(key: key);\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      flex: crossAxisCount == 1 ? 0 : 1,\n      child: Padding(\n        padding: crossAxisCount == 1\n            ? const EdgeInsets.fromLTRB(9, 9, 9, 4)\n            : const EdgeInsets.fromLTRB(5, 8, 5, 4),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            Text(\n              liveItem.title,\n              textAlign: TextAlign.start,\n              style: const TextStyle(\n                fontWeight: FontWeight.w500,\n                letterSpacing: 0.3,\n              ),\n              maxLines: crossAxisCount == 1 ? 1 : 2,\n              overflow: TextOverflow.ellipsis,\n            ),\n            if (crossAxisCount == 1) const SizedBox(height: 4),\n            Row(\n              children: [\n                Expanded(\n                  child: Text(\n                    liveItem.uname,\n                    textAlign: TextAlign.start,\n                    style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                    maxLines: 1,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n                if (crossAxisCount == 1) ...[\n                  Text(\n                    ' • ${liveItem!.areaName!}',\n                    style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                  ),\n                  Text(\n                    ' • ${liveItem!.watchedShow!['text_small']}人观看',\n                    style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                  ),\n                ]\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass VideoStat extends StatelessWidget {\n  final LiveItemModel? liveItem;\n\n  const VideoStat({\n    Key? key,\n    required this.liveItem,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 50,\n      padding: const EdgeInsets.only(top: 26, left: 10, right: 10),\n      decoration: const BoxDecoration(\n        gradient: LinearGradient(\n          begin: Alignment.topCenter,\n          end: Alignment.bottomCenter,\n          colors: <Color>[\n            Colors.transparent,\n            Colors.black54,\n          ],\n          tileMode: TileMode.mirror,\n        ),\n      ),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          Text(\n            liveItem!.areaName!,\n            style: const TextStyle(fontSize: 11, color: Colors.white),\n          ),\n          Text(\n            liveItem!.watchedShow!['text_small'],\n            style: const TextStyle(fontSize: 11, color: Colors.white),\n          ),\n        ],\n      ),\n\n      // child: RichText(\n      //   maxLines: 1,\n      //   textAlign: TextAlign.justify,\n      //   softWrap: false,\n      //   text: TextSpan(\n      //     style: const TextStyle(fontSize: 11, color: Colors.white),\n      //     children: [\n      //       TextSpan(text: liveItem!.areaName!),\n      //       TextSpan(text: liveItem!.watchedShow!['text_small']),\n      //     ],\n      //   ),\n      // ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/live_room/controller.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:ns_danmaku/ns_danmaku.dart';\nimport 'package:pilipala/http/constants.dart';\nimport 'package:pilipala/http/init.dart';\nimport 'package:pilipala/http/live.dart';\nimport 'package:pilipala/models/live/message.dart';\nimport 'package:pilipala/models/live/quality.dart';\nimport 'package:pilipala/models/live/room_info.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/plugin/pl_socket/index.dart';\nimport 'package:pilipala/utils/live.dart';\nimport '../../models/live/room_info_h5.dart';\nimport '../../utils/storage.dart';\nimport '../../utils/video_utils.dart';\n\nclass LiveRoomController extends GetxController {\n  String cover = '';\n  late int roomId;\n  dynamic liveItem;\n  late String heroTag;\n  double volume = 0.0;\n  // 静音状态\n  RxBool volumeOff = false.obs;\n  PlPlayerController plPlayerController = PlPlayerController(videoType: 'live');\n  Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs;\n  late bool enableCDN;\n  late int currentQn;\n  int? tempCurrentQn;\n  late List<Map<String, dynamic>> acceptQnList;\n  RxString currentQnDesc = ''.obs;\n  Box userInfoCache = GStrorage.userInfo;\n  int userId = 0;\n  PlSocket? plSocket;\n  List<String> danmuHostList = [];\n  String token = '';\n  // 弹幕消息列表\n  RxList<LiveMessageModel> messageList = <LiveMessageModel>[].obs;\n  DanmakuController? danmakuController;\n  // 输入控制器\n  TextEditingController inputController = TextEditingController();\n  // 加入直播间提示\n  RxMap<String, String> joinRoomTip = {'userName': '', 'message': ''}.obs;\n  // 直播间弹幕开关 默认打开\n  RxBool danmakuSwitch = true.obs;\n  late String buvid;\n  RxBool isPortrait = false.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    currentQn = setting.get(SettingBoxKey.defaultLiveQa,\n        defaultValue: LiveQuality.values.last.code);\n    roomId = int.parse(Get.parameters['roomid']!);\n    if (Get.arguments != null) {\n      liveItem = Get.arguments['liveItem'];\n      heroTag = Get.arguments['heroTag'] ?? '';\n      if (liveItem != null) {\n        cover = (liveItem.pic != null && liveItem.pic != '')\n            ? liveItem.pic\n            : (liveItem.cover != null && liveItem.cover != '')\n                ? liveItem.cover\n                : null;\n      }\n      Request.getBuvid().then((value) => buvid = value);\n    }\n    // CDN优化\n    enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);\n    final userInfo = userInfoCache.get('userInfoCache');\n    if (userInfo != null && userInfo.mid != null) {\n      userId = userInfo.mid;\n    }\n    liveDanmakuInfo().then((value) => initSocket());\n    danmakuSwitch.listen((p0) {\n      plPlayerController.isOpenDanmu.value = p0;\n    });\n  }\n\n  playerInit(source) async {\n    await plPlayerController.setDataSource(\n      DataSource(\n        videoSource: source,\n        audioSource: null,\n        type: DataSourceType.network,\n        httpHeaders: {\n          'user-agent':\n              'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',\n          'referer': HttpString.baseUrl\n        },\n      ),\n      // 硬解\n      enableHA: true,\n      autoplay: true,\n    );\n    plPlayerController.isOpenDanmu.value = danmakuSwitch.value;\n    heartBeat();\n  }\n\n  Future queryLiveInfo() async {\n    var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: currentQn);\n    if (res['status']) {\n      isPortrait.value = res['data'].isPortrait;\n      List<CodecItem> codec =\n          res['data'].playurlInfo.playurl.stream.first.format.first.codec;\n      CodecItem item = codec.first;\n      // 以服务端返回的码率为准\n      currentQn = item.currentQn!;\n      if (tempCurrentQn != null && tempCurrentQn == currentQn) {\n        SmartDialog.showToast('画质切换失败，请检查登录状态');\n      }\n      List acceptQn = item.acceptQn!;\n      acceptQnList = acceptQn.map((e) {\n        return {\n          'code': e,\n          'desc': LiveQuality.values\n              .firstWhere((element) => element.code == e)\n              .description,\n        };\n      }).toList();\n      currentQnDesc.value = LiveQuality.values\n          .firstWhere((element) => element.code == currentQn)\n          .description;\n      String videoUrl = enableCDN\n          ? VideoUtils.getCdnUrl(item)\n          : (item.urlInfo?.first.host)! +\n              item.baseUrl! +\n              item.urlInfo!.first.extra!;\n      await playerInit(videoUrl);\n      return res;\n    }\n  }\n\n  void setVolumn(value) {\n    if (value == 0) {\n      // 设置音量\n      volumeOff.value = false;\n    } else {\n      // 取消音量\n      volume = value;\n      volumeOff.value = true;\n    }\n  }\n\n  Future queryLiveInfoH5() async {\n    var res = await LiveHttp.liveRoomInfoH5(roomId: roomId);\n    if (res['status']) {\n      roomInfoH5.value = res['data'];\n    }\n    return res;\n  }\n\n  // 修改画质\n  void changeQn(int qn) async {\n    tempCurrentQn = currentQn;\n    if (currentQn == qn) {\n      return;\n    }\n    currentQn = qn;\n    currentQnDesc.value = LiveQuality.values\n        .firstWhere((element) => element.code == currentQn)\n        .description;\n    await queryLiveInfo();\n  }\n\n  Future liveDanmakuInfo() async {\n    var res = await LiveHttp.liveDanmakuInfo(roomId: roomId);\n    if (res['status']) {\n      danmuHostList = (res[\"data\"][\"host_list\"] as List)\n          .map<String>((e) => '${e[\"host\"]}:${e['wss_port']}')\n          .toList();\n      token = res[\"data\"][\"token\"];\n      return res;\n    }\n  }\n\n  // 建立socket\n  void initSocket() async {\n    final wsUrl = danmuHostList.isNotEmpty\n        ? danmuHostList.first\n        : \"broadcastlv.chat.bilibili.com\";\n    plSocket = PlSocket(\n      url: 'wss://$wsUrl/sub',\n      heartTime: 30,\n      onReadyCb: () {\n        joinRoom();\n      },\n      onMessageCb: (message) {\n        final List<LiveMessageModel>? liveMsg =\n            LiveUtils.decodeMessage(message);\n        if (liveMsg != null && liveMsg.isNotEmpty) {\n          if (liveMsg.first.type == LiveMessageType.online) {\n            print('当前直播间人气：${liveMsg.first.data}');\n          } else if (liveMsg.first.type == LiveMessageType.join ||\n              liveMsg.first.type == LiveMessageType.follow) {\n            // 每隔一秒依次liveMsg中的每一项赋给activeUserName\n\n            int index = 0;\n            Timer.periodic(const Duration(seconds: 2), (timer) {\n              if (index < liveMsg.length) {\n                if (liveMsg[index].type == LiveMessageType.join ||\n                    liveMsg[index].type == LiveMessageType.follow) {\n                  joinRoomTip.value = {\n                    'userName': liveMsg[index].userName,\n                    'message': liveMsg[index].message!,\n                  };\n                }\n                index++;\n              } else {\n                timer.cancel();\n              }\n            });\n\n            return;\n          }\n          // 过滤出聊天消息\n          var chatMessages =\n              liveMsg.where((msg) => msg.type == LiveMessageType.chat).toList();\n\n          // 添加到 messageList\n          messageList.addAll(chatMessages);\n\n          // 将 chatMessages 转换为 danmakuItems 列表\n          List<DanmakuItem> danmakuItems = chatMessages.map<DanmakuItem>((e) {\n            return DanmakuItem(\n              e.message ?? '',\n              color: Color.fromARGB(\n                255,\n                e.color.r,\n                e.color.g,\n                e.color.b,\n              ),\n            );\n          }).toList();\n\n          // 添加到 danmakuController\n          if (danmakuSwitch.value) {\n            danmakuController?.addItems(danmakuItems);\n          }\n        }\n      },\n      onErrorCb: (e) {\n        print('error: $e');\n      },\n    );\n    await plSocket?.connect();\n  }\n\n  void joinRoom() async {\n    var joinData = LiveUtils.encodeData(\n      json.encode({\n        \"uid\": userId,\n        \"roomid\": roomId,\n        \"protover\": 3,\n        \"buvid\": buvid,\n        \"platform\": \"web\",\n        \"type\": 2,\n        \"key\": token,\n      }),\n      7,\n    );\n    plSocket?.sendMessage(joinData);\n  }\n\n  // 发送弹幕\n  void sendMsg() async {\n    final msg = inputController.text;\n    if (msg.isEmpty) {\n      return;\n    }\n    final res = await LiveHttp.sendDanmaku(\n      roomId: roomId,\n      msg: msg,\n    );\n    if (res['status']) {\n      inputController.clear();\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  // 历史记录\n  void heartBeat() {\n    LiveHttp.liveRoomEntry(roomId: roomId);\n  }\n\n  String encodeToBase64(String input) {\n    List<int> bytes = utf8.encode(input);\n    String base64Str = base64.encode(bytes);\n    return base64Str;\n  }\n\n  @override\n  void onClose() {\n    heartBeat();\n    plSocket?.onClose();\n    super.onClose();\n  }\n}\n"
  },
  {
    "path": "lib/pages/live_room/index.dart",
    "content": "library liveroom;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/live_room/view.dart",
    "content": "import 'dart:io';\n\nimport 'package:floating/floating.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/live/message.dart';\nimport 'package:pilipala/pages/danmaku/index.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\n\nimport 'controller.dart';\nimport 'widgets/bottom_control.dart';\n\nclass LiveRoomPage extends StatefulWidget {\n  const LiveRoomPage({super.key});\n\n  @override\n  State<LiveRoomPage> createState() => _LiveRoomPageState();\n}\n\nclass _LiveRoomPageState extends State<LiveRoomPage>\n    with TickerProviderStateMixin {\n  final LiveRoomController _liveRoomController = Get.put(LiveRoomController());\n  late PlPlayerController plPlayerController;\n  late Future? _futureBuilder;\n  late Future? _futureBuilderFuture;\n\n  bool isShowCover = true;\n  bool isPlay = true;\n  Floating? floating;\n  final ScrollController _scrollController = ScrollController();\n  late AnimationController fabAnimationCtr;\n  bool _shouldAutoScroll = true;\n  final int roomId = int.parse(Get.parameters['roomid']!);\n\n  @override\n  void initState() {\n    super.initState();\n    if (Platform.isAndroid) {\n      floating = Floating();\n    }\n    videoSourceInit();\n    _futureBuilderFuture = _liveRoomController.queryLiveInfo();\n    // 监听滚动事件\n    _scrollController.addListener(_onScroll);\n    fabAnimationCtr = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n      value: 0.0,\n    );\n  }\n\n  Future<void> videoSourceInit() async {\n    _futureBuilder = _liveRoomController.queryLiveInfoH5();\n    plPlayerController = _liveRoomController.plPlayerController;\n  }\n\n  void _onScroll() {\n    // 反向时，展示按钮\n    if (_scrollController.position.userScrollDirection ==\n        ScrollDirection.forward) {\n      _shouldAutoScroll = false;\n      fabAnimationCtr.forward();\n    } else {\n      _shouldAutoScroll = true;\n      fabAnimationCtr.reverse();\n    }\n  }\n\n  // 监听messageList的变化，自动滚动到底部\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _liveRoomController.messageList.listen((_) {\n      if (_shouldAutoScroll) {\n        _scrollToBottom();\n      }\n    });\n  }\n\n  void _scrollToBottom() {\n    if (_scrollController.hasClients) {\n      _scrollController\n          .animateTo(\n        _scrollController.position.maxScrollExtent,\n        duration: const Duration(milliseconds: 300),\n        curve: Curves.easeOut,\n      )\n          .then((value) {\n        _shouldAutoScroll = true;\n        // fabAnimationCtr.forward();\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    plPlayerController.dispose();\n    if (floating != null) {\n      floating!.dispose();\n    }\n    _scrollController.dispose();\n    fabAnimationCtr.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget videoPlayerPanel = FutureBuilder(\n      future: _futureBuilderFuture,\n      builder: (BuildContext context, AsyncSnapshot snapshot) {\n        if (snapshot.hasData && snapshot.data['status']) {\n          plPlayerController = _liveRoomController.plPlayerController;\n          return PLVideoPlayer(\n            controller: plPlayerController,\n            alignment: _liveRoomController.isPortrait.value\n                ? Alignment.topCenter\n                : Alignment.center,\n            bottomControl: BottomControl(\n              controller: plPlayerController,\n              liveRoomCtr: _liveRoomController,\n              floating: floating,\n              onRefresh: () {\n                setState(() {\n                  _futureBuilderFuture = _liveRoomController.queryLiveInfo();\n                });\n              },\n            ),\n            danmuWidget: PlDanmaku(\n              cid: roomId,\n              playerController: plPlayerController,\n              type: 'live',\n              createdController: (e) {\n                _liveRoomController.danmakuController = e;\n              },\n            ),\n          );\n        } else {\n          return const SizedBox();\n        }\n      },\n    );\n\n    Widget childWhenDisabled = Scaffold(\n      primary: true,\n      backgroundColor: Colors.black,\n      body: Stack(\n        children: [\n          Obx(\n            () => Positioned(\n              left: 0,\n              right: 0,\n              bottom: 0,\n              child: _liveRoomController\n                              .roomInfoH5.value.roomInfo?.appBackground !=\n                          '' &&\n                      _liveRoomController\n                              .roomInfoH5.value.roomInfo?.appBackground !=\n                          null\n                  ? Opacity(\n                      opacity: 0.6,\n                      child: NetworkImgLayer(\n                        width: Get.width,\n                        height: Get.height,\n                        type: 'bg',\n                        src: _liveRoomController\n                                .roomInfoH5.value.roomInfo?.appBackground ??\n                            '',\n                      ),\n                    )\n                  : Opacity(\n                      opacity: 0.6,\n                      child: Image.asset(\n                        'assets/images/live/default_bg.webp',\n                        fit: BoxFit.cover,\n                        // width: Get.width,\n                        // height: Get.height,\n                      ),\n                    ),\n            ),\n          ),\n\n          Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Obx(\n                () => SizedBox(\n                  height: MediaQuery.of(context).padding.top +\n                      (_liveRoomController.isPortrait.value ||\n                              MediaQuery.of(context).orientation ==\n                                  Orientation.landscape\n                          ? 0\n                          : kToolbarHeight),\n                ),\n              ),\n              PopScope(\n                canPop: plPlayerController.isFullScreen.value != true,\n                onPopInvoked: (bool didPop) {\n                  if (plPlayerController.isFullScreen.value == true) {\n                    plPlayerController.triggerFullScreen(status: false);\n                  }\n                  if (MediaQuery.of(context).orientation ==\n                      Orientation.landscape) {\n                    verticalScreen();\n                  }\n                },\n                child: Obx(\n                  () => Container(\n                    width: Get.size.width,\n                    height: MediaQuery.of(context).orientation ==\n                            Orientation.landscape\n                        ? Get.size.height\n                        : !_liveRoomController.isPortrait.value\n                            ? Get.size.width * 9 / 16\n                            : Get.size.height -\n                                MediaQuery.of(context).padding.top,\n                    clipBehavior: Clip.hardEdge,\n                    decoration: const BoxDecoration(\n                      borderRadius: BorderRadius.all(Radius.circular(6)),\n                    ),\n                    child: videoPlayerPanel,\n                  ),\n                ),\n              ),\n            ],\n          ),\n          // 定位 快速滑动到底部\n          Positioned(\n            right: 20,\n            bottom: MediaQuery.of(context).padding.bottom + 80,\n            child: SlideTransition(\n              position: Tween<Offset>(\n                begin: const Offset(0, 4),\n                end: const Offset(0, 0),\n              ).animate(CurvedAnimation(\n                parent: fabAnimationCtr,\n                curve: Curves.easeInOut,\n              )),\n              child: ElevatedButton.icon(\n                onPressed: () {\n                  _scrollToBottom();\n                },\n                icon: const Icon(Icons.keyboard_arrow_down), // 图标\n                label: const Text('新消息'), // 文字\n                style: ElevatedButton.styleFrom(\n                  // primary: Colors.blue, // 按钮背景颜色\n                  // onPrimary: Colors.white, // 按钮文字颜色\n                  padding: const EdgeInsets.fromLTRB(14, 12, 20, 12), // 按钮内边距\n                ),\n              ),\n            ),\n          ),\n          // 顶栏\n          Positioned(\n            top: 0,\n            left: 0,\n            right: 0,\n            child: AppBar(\n              centerTitle: false,\n              titleSpacing: 0,\n              backgroundColor: Colors.transparent,\n              foregroundColor: Colors.white,\n              toolbarHeight:\n                  MediaQuery.of(context).orientation == Orientation.portrait\n                      ? 56\n                      : 0,\n              title: FutureBuilder(\n                future: _futureBuilder,\n                builder: (context, snapshot) {\n                  if (snapshot.data == null) {\n                    return const SizedBox();\n                  }\n                  Map data = snapshot.data as Map;\n                  if (data['status']) {\n                    return Obx(\n                      () => Row(\n                        children: [\n                          NetworkImgLayer(\n                            width: 34,\n                            height: 34,\n                            type: 'avatar',\n                            src: _liveRoomController\n                                .roomInfoH5.value.anchorInfo!.baseInfo!.face,\n                          ),\n                          const SizedBox(width: 10),\n                          Column(\n                            crossAxisAlignment: CrossAxisAlignment.start,\n                            children: [\n                              Text(\n                                _liveRoomController.roomInfoH5.value.anchorInfo!\n                                    .baseInfo!.uname!,\n                                style: const TextStyle(fontSize: 14),\n                              ),\n                              const SizedBox(height: 1),\n                              if (_liveRoomController\n                                      .roomInfoH5.value.watchedShow !=\n                                  null)\n                                Text(\n                                  _liveRoomController.roomInfoH5.value\n                                          .watchedShow!['text_large'] ??\n                                      '',\n                                  style: const TextStyle(fontSize: 12),\n                                ),\n                            ],\n                          ),\n                        ],\n                      ),\n                    );\n                  } else {\n                    return const SizedBox();\n                  }\n                },\n              ),\n            ),\n          ),\n          // 消息列表\n          Obx(\n            () => Positioned(\n              top: MediaQuery.of(context).padding.top +\n                  kToolbarHeight +\n                  (_liveRoomController.isPortrait.value\n                      ? Get.size.width\n                      : Get.size.width * 9 / 16),\n              bottom: 90 + MediaQuery.of(context).padding.bottom,\n              left: 0,\n              right: 0,\n              child: buildMessageListUI(\n                context,\n                _liveRoomController,\n                _scrollController,\n              ),\n            ),\n          ),\n          // 消息输入框\n          Visibility(\n            visible: MediaQuery.of(context).orientation == Orientation.portrait,\n            child: Positioned(\n              bottom: 0,\n              left: 0,\n              right: 0,\n              child: Container(\n                padding: EdgeInsets.only(\n                    left: 14,\n                    right: 14,\n                    top: 4,\n                    bottom: MediaQuery.of(context).padding.bottom + 20),\n                decoration: BoxDecoration(\n                  color: Colors.grey.withOpacity(0.1),\n                  borderRadius: const BorderRadius.all(Radius.circular(20)),\n                  border: Border(\n                    top: BorderSide(\n                      color: Colors.white.withOpacity(0.1),\n                    ),\n                  ),\n                ),\n                child: Row(\n                  children: [\n                    SizedBox(\n                      width: 34,\n                      height: 34,\n                      child: Obx(\n                        () => IconButton(\n                          style: ButtonStyle(\n                            padding: MaterialStateProperty.all(EdgeInsets.zero),\n                            backgroundColor: MaterialStateProperty.resolveWith(\n                                (Set<MaterialState> states) {\n                              return Colors.grey.withOpacity(0.1);\n                            }),\n                          ),\n                          onPressed: () {\n                            _liveRoomController.danmakuSwitch.value =\n                                !_liveRoomController.danmakuSwitch.value;\n                          },\n                          icon: Icon(\n                            _liveRoomController.danmakuSwitch.value\n                                ? Icons.subtitles_outlined\n                                : Icons.subtitles_off_outlined,\n                            size: 19,\n                            color: Colors.white,\n                          ),\n                        ),\n                      ),\n                    ),\n                    const SizedBox(width: 8),\n                    Expanded(\n                      child: TextField(\n                        controller: _liveRoomController.inputController,\n                        style:\n                            const TextStyle(color: Colors.white, fontSize: 13),\n                        decoration: InputDecoration(\n                          hintText: '发送弹幕',\n                          hintStyle: TextStyle(\n                            color: Colors.white.withOpacity(0.6),\n                          ),\n                          border: InputBorder.none,\n                        ),\n                      ),\n                    ),\n                    SizedBox(\n                      width: 34,\n                      height: 34,\n                      child: IconButton(\n                        style: ButtonStyle(\n                          padding: MaterialStateProperty.all(EdgeInsets.zero),\n                        ),\n                        onPressed: () => _liveRoomController.sendMsg(),\n                        icon: const Icon(\n                          Icons.send,\n                          color: Colors.white,\n                          size: 20,\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n    if (Platform.isAndroid) {\n      return PiPSwitcher(\n        childWhenDisabled: childWhenDisabled,\n        childWhenEnabled: videoPlayerPanel,\n        floating: floating,\n      );\n    } else {\n      return childWhenDisabled;\n    }\n  }\n}\n\nWidget buildMessageListUI(\n  BuildContext context,\n  LiveRoomController liveRoomController,\n  ScrollController scrollController,\n) {\n  return Expanded(\n    child: Obx(\n      () => MediaQuery.removePadding(\n        context: context,\n        removeTop: true,\n        removeBottom: true,\n        child: ShaderMask(\n          shaderCallback: (Rect bounds) {\n            return LinearGradient(\n              begin: Alignment.topCenter,\n              end: Alignment.bottomCenter,\n              colors: [\n                Colors.transparent,\n                Colors.black.withOpacity(0.5),\n                Colors.black,\n              ],\n              stops: const [0.01, 0.05, 0.2],\n            ).createShader(bounds);\n          },\n          blendMode: BlendMode.dstIn,\n          child: GestureDetector(\n            onTap: () {\n              // 键盘失去焦点\n              FocusScope.of(context).requestFocus(FocusNode());\n            },\n            child: ListView.builder(\n              controller: scrollController,\n              itemCount: liveRoomController.messageList.length,\n              itemBuilder: (context, index) {\n                final LiveMessageModel liveMsgItem =\n                    liveRoomController.messageList[index];\n                return Align(\n                  alignment: Alignment.centerLeft,\n                  child: Container(\n                    decoration: BoxDecoration(\n                      color: liveRoomController.isPortrait.value\n                          ? Colors.black.withOpacity(0.3)\n                          : Colors.grey.withOpacity(0.1),\n                      borderRadius: const BorderRadius.all(Radius.circular(20)),\n                    ),\n                    margin: EdgeInsets.only(\n                      top: index == 0 ? 20.0 : 0.0,\n                      bottom: 6.0,\n                      left: 14.0,\n                      right: 14.0,\n                    ),\n                    padding: const EdgeInsets.symmetric(\n                      vertical: 3.0,\n                      horizontal: 10.0,\n                    ),\n                    child: Text.rich(\n                      TextSpan(\n                        style: const TextStyle(color: Colors.white),\n                        children: [\n                          TextSpan(\n                            text: '${liveMsgItem.userName}: ',\n                            style: TextStyle(\n                              color: Colors.white.withOpacity(0.6),\n                            ),\n                            recognizer: TapGestureRecognizer()\n                              ..onTap = () {\n                                // 处理点击事件\n                                print('Text clicked');\n                              },\n                          ),\n                          TextSpan(\n                            children: [\n                              ...buildMessageTextSpan(context, liveMsgItem)\n                            ],\n                            // text: liveMsgItem.message,\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                );\n              },\n            ),\n          ),\n        ),\n      ),\n    ),\n  );\n}\n\nList<InlineSpan> buildMessageTextSpan(\n  BuildContext context,\n  LiveMessageModel liveMsgItem,\n) {\n  final List<InlineSpan> inlineSpanList = [];\n\n  // 是否包含表情包\n  if (liveMsgItem.emots == null) {\n    // 没有表情包的消息\n    inlineSpanList.add(\n      TextSpan(text: liveMsgItem.message ?? ''),\n    );\n  } else {\n    // 有表情包的消息 使用正则匹配 表情包用图片渲染\n    final List<String> emotsKeys = liveMsgItem.emots!.keys.toList();\n    final RegExp pattern = RegExp(emotsKeys.map(RegExp.escape).join('|'));\n\n    liveMsgItem.message?.splitMapJoin(\n      pattern,\n      onMatch: (Match match) {\n        final emoteItem = liveMsgItem.emots![match.group(0)];\n        if (emoteItem != null) {\n          inlineSpanList.add(\n            WidgetSpan(\n              child: NetworkImgLayer(\n                width: emoteItem['width'].toDouble(),\n                height: emoteItem['height'].toDouble(),\n                type: 'emote',\n                src: emoteItem['url'],\n              ),\n            ),\n          );\n        }\n        return '';\n      },\n      onNonMatch: (String nonMatch) {\n        inlineSpanList.add(\n          TextSpan(text: nonMatch),\n        );\n        return nonMatch;\n      },\n    );\n  }\n\n  return inlineSpanList;\n}\n"
  },
  {
    "path": "lib/pages/live_room/widgets/bottom_control.dart",
    "content": "import 'dart:io';\n\nimport 'package:floating/floating.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/video/play/url.dart';\nimport 'package:pilipala/pages/live_room/index.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass BottomControl extends StatefulWidget implements PreferredSizeWidget {\n  final PlPlayerController? controller;\n  final LiveRoomController? liveRoomCtr;\n  final Floating? floating;\n  final Function? onRefresh;\n  const BottomControl({\n    this.controller,\n    this.liveRoomCtr,\n    this.floating,\n    this.onRefresh,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  State<BottomControl> createState() => _BottomControlState();\n\n  @override\n  Size get preferredSize => throw UnimplementedError();\n}\n\nclass _BottomControlState extends State<BottomControl> {\n  late PlayUrlModel videoInfo;\n  TextStyle subTitleStyle = const TextStyle(fontSize: 12);\n  TextStyle titleStyle = const TextStyle(fontSize: 14);\n  Size get preferredSize => const Size(double.infinity, kToolbarHeight);\n  Box localCache = GStrorage.localCache;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBar(\n      backgroundColor: Colors.transparent,\n      foregroundColor: Colors.white,\n      elevation: 0,\n      scrolledUnderElevation: 0,\n      primary: false,\n      centerTitle: false,\n      automaticallyImplyLeading: false,\n      titleSpacing: 14,\n      title: Row(\n        children: [\n          // ComBtn(\n          //   icon: const Icon(\n          //     Icons.subtitles_outlined,\n          //     size: 18,\n          //     color: Colors.white,\n          //   ),\n          //   fuc: () => Get.back(),\n          // ),\n          ComBtn(\n            icon: const Icon(\n              Icons.refresh_outlined,\n              size: 18,\n              color: Colors.white,\n            ),\n            fuc: widget.onRefresh,\n          ),\n          const Spacer(),\n          // ComBtn(\n          //   icon: const Icon(\n          //     Icons.hd_outlined,\n          //     size: 18,\n          //     color: Colors.white,\n          //   ),\n          //   fuc: () => {},\n          // ),\n          // const SizedBox(width: 4),\n          // Obx(\n          //   () => ComBtn(\n          //     icon: Icon(\n          //       widget.liveRoomCtr!.volumeOff.value\n          //           ? Icons.volume_off_outlined\n          //           : Icons.volume_up_outlined,\n          //       size: 18,\n          //       color: Colors.white,\n          //     ),\n          //     fuc: () => {},\n          //   ),\n          // ),\n          // const SizedBox(width: 4),\n          SizedBox(\n            width: 30,\n            child: PopupMenuButton<int>(\n              padding: EdgeInsets.zero,\n              onSelected: (value) {\n                widget.liveRoomCtr!.changeQn(value);\n              },\n              child: Obx(\n                () => Text(\n                  widget.liveRoomCtr!.currentQnDesc.value,\n                  style: const TextStyle(color: Colors.white, fontSize: 13),\n                ),\n              ),\n              itemBuilder: (BuildContext context) {\n                return widget.liveRoomCtr!.acceptQnList.map((e) {\n                  return PopupMenuItem<int>(\n                    value: e['code'],\n                    child: Text(e['desc']),\n                  );\n                }).toList();\n              },\n            ),\n          ),\n          const SizedBox(width: 10),\n          if (Platform.isAndroid) ...[\n            SizedBox(\n              width: 34,\n              height: 34,\n              child: IconButton(\n                style: ButtonStyle(\n                  padding: MaterialStateProperty.all(EdgeInsets.zero),\n                ),\n                onPressed: () async {\n                  bool canUsePiP = false;\n                  widget.controller!.hiddenControls(false);\n                  try {\n                    canUsePiP = await widget.floating!.isPipAvailable;\n                  } on PlatformException catch (_) {\n                    canUsePiP = false;\n                  }\n                  if (canUsePiP) {\n                    await widget.floating!.enable();\n                  } else {}\n                },\n                icon: const Icon(\n                  Icons.picture_in_picture_outlined,\n                  size: 18,\n                  color: Colors.white,\n                ),\n              ),\n            ),\n            const SizedBox(width: 10),\n          ],\n          ComBtn(\n            icon: const Icon(\n              Icons.fullscreen,\n              size: 20,\n              color: Colors.white,\n            ),\n            fuc: () => widget.controller!.triggerFullScreen(\n                status: !(widget.controller!.isFullScreen.value)),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/login/controller.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:encrypt/encrypt.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/login.dart';\nimport 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';\nimport 'package:pilipala/models/login/index.dart';\nimport 'package:pilipala/utils/login.dart';\n\nclass LoginPageController extends GetxController {\n  final GlobalKey mobFormKey = GlobalKey<FormState>();\n  final GlobalKey passwordFormKey = GlobalKey<FormState>();\n  final GlobalKey msgCodeFormKey = GlobalKey<FormState>();\n\n  final TextEditingController mobTextController = TextEditingController();\n  final TextEditingController passwordTextController = TextEditingController();\n  final TextEditingController msgCodeTextController = TextEditingController();\n\n  final FocusNode mobTextFieldNode = FocusNode();\n  final FocusNode passwordTextFieldNode = FocusNode();\n  final FocusNode msgCodeTextFieldNode = FocusNode();\n\n  final PageController pageViewController = PageController();\n\n  RxInt currentIndex = 0.obs;\n\n  final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();\n\n  // 倒计时60s\n  RxInt seconds = 60.obs;\n  Timer? timer;\n  RxBool smsCodeSendStatus = false.obs;\n\n  // 默认密码登录\n  RxInt loginType = 0.obs;\n\n  late String captchaKey;\n\n  late int tel;\n  late int webSmsCode;\n\n  RxInt validSeconds = 180.obs;\n  Timer? validTimer;\n  late String qrcodeKey;\n  RxBool passwordVisible = false.obs;\n\n  // 监听pageView切换\n  void onPageChange(int index) {\n    currentIndex.value = index;\n  }\n\n  // 输入手机号 下一页\n  void nextStep() async {\n    if ((mobFormKey.currentState as FormState).validate()) {\n      await pageViewController.animateToPage(\n        1,\n        duration: const Duration(microseconds: 3000),\n        curve: Curves.easeInOut,\n      );\n      passwordTextFieldNode.requestFocus();\n      (mobFormKey.currentState as FormState).save();\n    }\n  }\n\n  // 上一页\n  void previousPage() async {\n    passwordTextFieldNode.unfocus();\n    await Future.delayed(const Duration(milliseconds: 200));\n    pageViewController.animateToPage(\n      0,\n      duration: const Duration(microseconds: 300),\n      curve: Curves.easeInOut,\n    );\n  }\n\n  // 切换登录方式\n  void changeLoginType() {\n    loginType.value = loginType.value == 0 ? 1 : 0;\n    if (loginType.value == 0) {\n      passwordTextFieldNode.requestFocus();\n    } else {\n      msgCodeTextFieldNode.requestFocus();\n    }\n  }\n\n  // app端密码登录\n  void loginInByAppPassword() async {\n    if ((passwordFormKey.currentState as FormState).validate()) {\n      var webKeyRes = await LoginHttp.getWebKey();\n      if (webKeyRes['status']) {\n        String rhash = webKeyRes['data']['hash'];\n        String key = webKeyRes['data']['key'];\n        LoginHttp.loginInByMobPwd(\n          tel: mobTextController.text,\n          password: passwordTextController.text,\n          key: key,\n          rhash: rhash,\n        );\n      } else {\n        SmartDialog.showToast(webKeyRes['msg']);\n      }\n    }\n  }\n\n  // web端密码登录\n  void loginInByWebPassword() async {\n    if ((passwordFormKey.currentState as FormState).validate()) {\n      getCaptcha((data) async {\n        CaptchaDataModel captchaData = data;\n        var webKeyRes = await LoginHttp.getWebKey();\n        if (webKeyRes['status']) {\n          String rhash = webKeyRes['data']['hash'];\n          String key = webKeyRes['data']['key'];\n          dynamic publicKey = RSAKeyParser().parse(key);\n          String passwordEncryptyed = Encrypter(RSA(publicKey: publicKey))\n              .encrypt(rhash + passwordTextController.text)\n              .base64;\n          var res = await LoginHttp.loginInByWebPwd(\n            username: tel,\n            password: passwordEncryptyed,\n            token: captchaData.token!,\n            challenge: captchaData.geetest!.challenge!,\n            validate: captchaData.validate!,\n            seccode: captchaData.seccode!,\n          );\n          if (res['status']) {\n            await LoginUtils.confirmLogin('', null);\n          } else {\n            await SmartDialog.showToast(res['msg']);\n            if (res.containsKey('code') && res['code'] == 1) {\n              Get.toNamed('/webview', parameters: {\n                'url': res['data']['data']['url'],\n                'type': 'url',\n                'pageTitle': '登录验证',\n              });\n            }\n          }\n        } else {\n          SmartDialog.showToast(webKeyRes['msg']);\n        }\n      });\n    }\n  }\n\n  // web端验证码登录\n  void loginInByCode() async {\n    if ((msgCodeFormKey.currentState as FormState).validate()) {\n      (msgCodeFormKey.currentState as FormState).save();\n      var res = await LoginHttp.loginInByWebSmsCode(\n        cid: 86,\n        tel: tel,\n        code: webSmsCode,\n        captchaKey: captchaKey,\n      );\n      if (res['status']) {\n        await LoginUtils.confirmLogin('', null);\n      } else {\n        SmartDialog.showToast(res['msg']);\n      }\n    }\n  }\n\n  // 获取app端验证码\n  void getAppMsgCode() async {\n    getCaptcha((data) async {\n      CaptchaDataModel captchaData = data;\n      var res = await LoginHttp.sendAppSmsCode(\n        cid: 86,\n        tel: tel,\n        token: captchaData.token!,\n        challenge: captchaData.geetest!.challenge!,\n        validate: captchaData.validate!,\n        seccode: captchaData.seccode!,\n      );\n      print(res);\n    });\n  }\n\n  // 申请极验验证码\n  Future getCaptcha(oncall) async {\n    SmartDialog.showLoading(msg: '请求中...');\n    var result = await LoginHttp.queryCaptcha();\n    if (result['status']) {\n      CaptchaDataModel captchaData = result['data'];\n      var registerData = Gt3RegisterData(\n        challenge: captchaData.geetest!.challenge,\n        gt: captchaData.geetest!.gt!,\n        success: true,\n      );\n      captcha.addEventHandler(onShow: (Map<String, dynamic> message) async {\n        SmartDialog.dismiss();\n      }, onClose: (Map<String, dynamic> message) async {\n        SmartDialog.showToast('取消验证');\n      }, onResult: (Map<String, dynamic> message) async {\n        debugPrint(\"Captcha result: $message\");\n        String code = message[\"code\"];\n        if (code == \"1\") {\n          // 发送 message[\"result\"] 中的数据向 B 端的业务服务接口进行查询\n          SmartDialog.showToast('验证成功');\n          captchaData.validate = message['result']['geetest_validate'];\n          captchaData.seccode = message['result']['geetest_seccode'];\n          captchaData.geetest!.challenge =\n              message['result']['geetest_challenge'];\n          oncall(captchaData);\n        } else {\n          // 终端用户完成验证失败，自动重试 If the verification fails, it will be automatically retried.\n          debugPrint(\"Captcha result code : $code\");\n        }\n      }, onError: (Map<String, dynamic> message) async {\n        String code = message[\"code\"];\n\n        // 处理验证中返回的错误 Handling errors returned in verification\n        if (Platform.isAndroid) {\n          // Android 平台\n          if (code == \"-2\") {\n            // Dart 调用异常 Call exception\n          } else if (code == \"-1\") {\n            // Gt3RegisterData 参数不合法 Parameter is invalid\n          } else if (code == \"201\") {\n            // 网络无法访问 Network inaccessible\n          } else if (code == \"202\") {\n            // Json 解析错误 Analysis error\n          } else if (code == \"204\") {\n            // WebView 加载超时，请检查是否混淆极验 SDK   Load timed out\n          } else if (code == \"204_1\") {\n            // WebView 加载前端页面错误，请查看日志 Error loading front-end page, please check the log\n          } else if (code == \"204_2\") {\n            // WebView 加载 SSLError\n          } else if (code == \"206\") {\n            // gettype 接口错误或返回为 null   API error or return null\n          } else if (code == \"207\") {\n            // getphp 接口错误或返回为 null    API error or return null\n          } else if (code == \"208\") {\n            // ajax 接口错误或返回为 null      API error or return null\n          } else {\n            // 更多错误码参考开发文档  More error codes refer to the development document\n            // https://docs.geetest.com/sensebot/apirefer/errorcode/android\n          }\n        }\n\n        if (Platform.isIOS) {\n          // iOS 平台\n          if (code == \"-1009\") {\n            // 网络无法访问 Network inaccessible\n          } else if (code == \"-1004\") {\n            // 无法查找到 HOST  Unable to find HOST\n          } else if (code == \"-1002\") {\n            // 非法的 URL  Illegal URL\n          } else if (code == \"-1001\") {\n            // 网络超时 Network timeout\n          } else if (code == \"-999\") {\n            // 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation\n          } else if (code == \"-21\") {\n            // 使用了重复的 challenge   Duplicate challenges are used\n            // 检查获取 challenge 是否进行了缓存  Check if the fetch challenge is cached\n          } else if (code == \"-20\") {\n            // 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again\n          } else if (code == \"-10\") {\n            // 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification\n          } else if (code == \"-2\") {\n            // Dart 调用异常 Call exception\n          } else if (code == \"-1\") {\n            // Gt3RegisterData 参数不合法  Parameter is invalid\n          } else {\n            // 更多错误码参考开发文档 More error codes refer to the development document\n            // https://docs.geetest.com/sensebot/apirefer/errorcode/ios\n          }\n        }\n      });\n      captcha.startCaptcha(registerData);\n    } else {}\n  }\n\n  // 获取web端验证码\n  void getWebMsgCode() async {\n    getCaptcha((data) async {\n      CaptchaDataModel captchaData = data;\n      var res = await LoginHttp.sendWebSmsCode(\n        cid: 86,\n        tel: tel,\n        token: captchaData.token!,\n        challenge: captchaData.geetest!.challenge!,\n        validate: captchaData.validate!,\n        seccode: captchaData.seccode!,\n      );\n      if (res['status']) {\n        captchaKey = res['data']['captcha_key'];\n        SmartDialog.showToast('验证码已发送');\n        // 倒计时60s\n        smsCodeSendStatus.value = true;\n        startTimer();\n      } else {\n        SmartDialog.showToast(res['msg']);\n      }\n    });\n  }\n\n  // 验证码倒计时\n  void startTimer() {\n    timer = Timer.periodic(const Duration(seconds: 1), (timer) {\n      if (seconds.value > 0) {\n        seconds.value--;\n      } else {\n        seconds.value = 60;\n        smsCodeSendStatus.value = false;\n        timer.cancel();\n      }\n    });\n  }\n\n  // 获取登录二维码\n  Future getWebQrcode() async {\n    var res = await LoginHttp.getWebQrcode();\n    validSeconds.value = 180;\n    if (res['status']) {\n      qrcodeKey = res['data']['qrcode_key'];\n      validTimer = Timer.periodic(const Duration(seconds: 1), (validTimer) {\n        if (validSeconds.value > 0) {\n          validSeconds.value--;\n          queryWebQrcodeStatus();\n        } else {\n          getWebQrcode();\n          validTimer.cancel();\n        }\n      });\n      return res;\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  // 轮询二维码登录状态\n  Future queryWebQrcodeStatus() async {\n    var res = await LoginHttp.queryWebQrcodeStatus(qrcodeKey);\n    if (res['status']) {\n      await LoginUtils.confirmLogin('', null);\n      validTimer?.cancel();\n      Get.back();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/login/index.dart",
    "content": "library login;\n\nexport './controller.dart';\nexport 'view.dart';\n"
  },
  {
    "path": "lib/pages/login/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:qr_flutter/qr_flutter.dart';\n\nimport 'controller.dart';\n\nclass LoginPage extends StatefulWidget {\n  const LoginPage({super.key});\n\n  @override\n  State<LoginPage> createState() => _LoginPageState();\n}\n\nclass _LoginPageState extends State<LoginPage> {\n  final LoginPageController _loginPageCtr = Get.put(LoginPageController());\n\n  @override\n  void dispose() {\n    _loginPageCtr.validTimer?.cancel();\n    _loginPageCtr.timer?.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        leading: Obx(\n          () => _loginPageCtr.currentIndex.value == 0\n              ? IconButton(\n                  onPressed: () async {\n                    _loginPageCtr.mobTextFieldNode.unfocus();\n                    await Future.delayed(const Duration(milliseconds: 200));\n                    Get.back();\n                  },\n                  icon: const Icon(Icons.close_outlined),\n                )\n              : IconButton(\n                  onPressed: () => _loginPageCtr.previousPage(),\n                  icon: const Icon(Icons.arrow_back),\n                ),\n        ),\n        actions: [\n          IconButton(\n            tooltip: '浏览器打开',\n            onPressed: () {\n              Get.offNamed(\n                '/webview',\n                parameters: {\n                  'url': 'https://passport.bilibili.com/h5-app/passport/login',\n                  'type': 'login',\n                  'pageTitle': '登录bilibili',\n                },\n              );\n            },\n            icon: const Icon(Icons.language, size: 20),\n          ),\n          IconButton(\n            tooltip: '二维码登录',\n            onPressed: () {\n              showDialog(\n                context: context,\n                builder: (context) {\n                  return StatefulBuilder(\n                      builder: (context, StateSetter setState) {\n                    return AlertDialog(\n                      title: Row(\n                        children: [\n                          const Text('扫码登录'),\n                          IconButton(\n                            onPressed: () {\n                              setState(() {});\n                            },\n                            icon: const Icon(Icons.refresh),\n                          ),\n                        ],\n                      ),\n                      contentPadding: const EdgeInsets.fromLTRB(0, 0, 0, 4),\n                      content: AspectRatio(\n                        aspectRatio: 1,\n                        child: Container(\n                          width: 200,\n                          padding: const EdgeInsets.all(12),\n                          child: FutureBuilder(\n                            future: _loginPageCtr.getWebQrcode(),\n                            builder: (context, snapshot) {\n                              if (snapshot.connectionState ==\n                                  ConnectionState.done) {\n                                if (snapshot.data == null) {\n                                  return const SizedBox();\n                                }\n                                Map data = snapshot.data as Map;\n                                return QrImageView(\n                                  data: data['data']['url'],\n                                  backgroundColor: Colors.white,\n                                );\n                              } else {\n                                return const Center(\n                                  child: SizedBox(\n                                    width: 40,\n                                    height: 40,\n                                    child: CircularProgressIndicator(),\n                                  ),\n                                );\n                              }\n                            },\n                          ),\n                        ),\n                      ),\n                      actions: [\n                        TextButton(\n                          onPressed: () {},\n                          child: Obx(() {\n                            return Text(\n                              '有效期: ${_loginPageCtr.validSeconds.value}s',\n                              style: Theme.of(context).textTheme.titleMedium,\n                            );\n                          }),\n                        ),\n                        TextButton(\n                          onPressed: () {},\n                          child: Text(\n                            '检查登录状态',\n                            style: TextStyle(\n                              fontSize: Theme.of(context)\n                                  .textTheme\n                                  .titleMedium!\n                                  .fontSize,\n                            ),\n                          ),\n                        )\n                      ],\n                    );\n                  });\n                },\n              ).then((value) {\n                _loginPageCtr.validTimer!.cancel();\n              });\n            },\n            icon: const Icon(Icons.qr_code, size: 20),\n          ),\n          const SizedBox(width: 22),\n        ],\n      ),\n      body: PageView(\n        physics: const NeverScrollableScrollPhysics(),\n        controller: _loginPageCtr.pageViewController,\n        onPageChanged: (int index) => _loginPageCtr.onPageChange(index),\n        children: [\n          Padding(\n            padding: EdgeInsets.only(\n              left: 20,\n              right: 20,\n              top: 10,\n              bottom: MediaQuery.of(context).padding.bottom + 10,\n            ),\n            child: Form(\n              key: _loginPageCtr.mobFormKey,\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                mainAxisSize: MainAxisSize.max,\n                children: [\n                  Text(\n                    '登录',\n                    style: Theme.of(context).textTheme.titleLarge!.copyWith(\n                        letterSpacing: 1,\n                        height: 2.1,\n                        fontSize: 34,\n                        fontWeight: FontWeight.w500),\n                  ),\n                  Text(\n                    '请使用您的 BiliBili 账号登录。',\n                    style: Theme.of(context).textTheme.titleSmall!,\n                  ),\n                  Container(\n                    margin: const EdgeInsets.only(top: 38, bottom: 15),\n                    child: TextFormField(\n                      controller: _loginPageCtr.mobTextController,\n                      focusNode: _loginPageCtr.mobTextFieldNode,\n                      keyboardType: TextInputType.number,\n                      decoration: InputDecoration(\n                        isDense: true,\n                        labelText: '输入手机号码',\n                        border: OutlineInputBorder(\n                          borderRadius: BorderRadius.circular(6.0),\n                        ),\n                      ),\n                      // 校验用户名\n                      validator: (v) {\n                        return v!.trim().isNotEmpty ? null : \"手机号码不能为空\";\n                      },\n                      onSaved: (val) => _loginPageCtr.tel = int.parse(val!),\n                      onEditingComplete: () {\n                        _loginPageCtr.nextStep();\n                      },\n                    ),\n                  ),\n                  const Spacer(),\n                  Row(\n                    mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                    children: [\n                      TextButton(onPressed: () {}, child: const Text('中国大陆')),\n                      TextButton(\n                        style: TextButton.styleFrom(\n                          padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),\n                          foregroundColor:\n                              Theme.of(context).colorScheme.onPrimary,\n                          backgroundColor:\n                              Theme.of(context).colorScheme.primary, // 设置按钮背景色\n                        ),\n                        onPressed: () => _loginPageCtr.nextStep(),\n                        child: const Text('下一步'),\n                      )\n                    ],\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Padding(\n            padding: EdgeInsets.only(\n              left: 20,\n              right: 20,\n              top: 10,\n              bottom: MediaQuery.of(context).padding.bottom + 10,\n            ),\n            child: Obx(\n              () => _loginPageCtr.loginType.value == 0\n                  ? Form(\n                      key: _loginPageCtr.passwordFormKey,\n                      child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        mainAxisSize: MainAxisSize.max,\n                        children: [\n                          Row(\n                            children: [\n                              Text(\n                                '密码登录',\n                                style: Theme.of(context)\n                                    .textTheme\n                                    .titleLarge!\n                                    .copyWith(\n                                        letterSpacing: 1,\n                                        height: 2.1,\n                                        fontSize: 34,\n                                        fontWeight: FontWeight.w500),\n                              ),\n                              const SizedBox(width: 4),\n                              IconButton(\n                                style: ButtonStyle(\n                                  backgroundColor:\n                                      MaterialStateProperty.resolveWith(\n                                          (states) {\n                                    return Theme.of(context)\n                                        .colorScheme\n                                        .primary\n                                        .withOpacity(0.1);\n                                  }),\n                                ),\n                                onPressed: () =>\n                                    _loginPageCtr.changeLoginType(),\n                                icon: const Icon(Icons.swap_vert_outlined),\n                              )\n                            ],\n                          ),\n                          Text(\n                            '请输入您的 BiliBili 密码。',\n                            style: Theme.of(context).textTheme.titleSmall!,\n                          ),\n                          Container(\n                            margin: const EdgeInsets.only(top: 38, bottom: 15),\n                            child: Obx(() => TextFormField(\n                                  controller:\n                                      _loginPageCtr.passwordTextController,\n                                  focusNode:\n                                      _loginPageCtr.passwordTextFieldNode,\n                                  keyboardType: TextInputType.visiblePassword,\n                                  obscureText:\n                                      _loginPageCtr.passwordVisible.value,\n                                  decoration: InputDecoration(\n                                    isDense: true,\n                                    labelText: '输入密码',\n                                    border: OutlineInputBorder(\n                                      borderRadius: BorderRadius.circular(6.0),\n                                    ),\n                                    suffixIcon: IconButton(\n                                      icon: Icon(\n                                        _loginPageCtr.passwordVisible.value\n                                            ? Icons.visibility\n                                            : Icons.visibility_off,\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .primary,\n                                      ),\n                                      onPressed: () {\n                                        _loginPageCtr.passwordVisible.value =\n                                            !_loginPageCtr\n                                                .passwordVisible.value;\n                                      },\n                                    ),\n                                  ),\n                                  // 校验用户名\n                                  validator: (v) {\n                                    return v!.trim().isNotEmpty\n                                        ? null\n                                        : \"密码不能为空\";\n                                  },\n                                  onSaved: (val) {\n                                    print(val);\n                                  },\n                                )),\n                          ),\n                          const Spacer(),\n                          Row(\n                            mainAxisAlignment: MainAxisAlignment.end,\n                            children: [\n                              TextButton(\n                                onPressed: () => _loginPageCtr.previousPage(),\n                                child: const Text('上一步'),\n                              ),\n                              const SizedBox(width: 15),\n                              TextButton(\n                                style: TextButton.styleFrom(\n                                  padding:\n                                      const EdgeInsets.fromLTRB(20, 0, 20, 0),\n                                  foregroundColor:\n                                      Theme.of(context).colorScheme.onPrimary,\n                                  backgroundColor: Theme.of(context)\n                                      .colorScheme\n                                      .primary, // 设置按钮背景色\n                                ),\n                                onPressed: () =>\n                                    _loginPageCtr.loginInByWebPassword(),\n                                child: const Text('确认登录'),\n                              )\n                            ],\n                          ),\n                        ],\n                      ),\n                    )\n                  : Form(\n                      key: _loginPageCtr.msgCodeFormKey,\n                      child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        mainAxisSize: MainAxisSize.max,\n                        children: [\n                          Row(\n                            children: [\n                              Text(\n                                '验证码登录',\n                                style: Theme.of(context)\n                                    .textTheme\n                                    .titleLarge!\n                                    .copyWith(\n                                        letterSpacing: 1,\n                                        height: 2.1,\n                                        fontSize: 34,\n                                        fontWeight: FontWeight.w500),\n                              ),\n                              const SizedBox(width: 4),\n                              IconButton(\n                                style: ButtonStyle(\n                                  backgroundColor:\n                                      MaterialStateProperty.resolveWith(\n                                          (states) {\n                                    return Theme.of(context)\n                                        .colorScheme\n                                        .primary\n                                        .withOpacity(0.1);\n                                  }),\n                                ),\n                                onPressed: () =>\n                                    _loginPageCtr.changeLoginType(),\n                                icon: const Icon(Icons.swap_vert_outlined),\n                              )\n                            ],\n                          ),\n                          Text(\n                            '请输入收到到验证码。',\n                            style: Theme.of(context).textTheme.titleSmall!,\n                          ),\n                          Container(\n                            margin: const EdgeInsets.only(top: 38, bottom: 15),\n                            child: Stack(\n                              children: [\n                                TextFormField(\n                                  controller:\n                                      _loginPageCtr.msgCodeTextController,\n                                  focusNode: _loginPageCtr.msgCodeTextFieldNode,\n                                  maxLength: 6,\n                                  keyboardType: TextInputType.number,\n                                  decoration: InputDecoration(\n                                    isDense: true,\n                                    labelText: '输入验证码',\n                                    border: OutlineInputBorder(\n                                      borderRadius: BorderRadius.circular(6.0),\n                                    ),\n                                  ),\n                                  // 校验用户名\n                                  validator: (v) {\n                                    return v!.trim().isNotEmpty\n                                        ? null\n                                        : \"验证码不能为空\";\n                                  },\n                                  onSaved: (val) => _loginPageCtr.webSmsCode =\n                                      int.parse(val!),\n                                ),\n                                Obx(() {\n                                  return Positioned(\n                                    right: 8,\n                                    top: 0,\n                                    child: Center(\n                                      child: TextButton(\n                                          onPressed: _loginPageCtr\n                                                  .smsCodeSendStatus.value\n                                              ? null\n                                              : () =>\n                                                  _loginPageCtr.getWebMsgCode(),\n                                          child: _loginPageCtr\n                                                  .smsCodeSendStatus.value\n                                              ? Text(\n                                                  '重新获取(${_loginPageCtr.seconds.value}s)')\n                                              : const Text('获取验证码')),\n                                    ),\n                                  );\n                                })\n                              ],\n                            ),\n                          ),\n                          const Spacer(),\n                          Row(\n                            mainAxisAlignment: MainAxisAlignment.end,\n                            children: [\n                              TextButton(\n                                onPressed: () => _loginPageCtr.previousPage(),\n                                child: const Text('上一步'),\n                              ),\n                              const SizedBox(width: 15),\n                              TextButton(\n                                style: TextButton.styleFrom(\n                                  padding:\n                                      const EdgeInsets.fromLTRB(20, 0, 20, 0),\n                                  foregroundColor:\n                                      Theme.of(context).colorScheme.onPrimary,\n                                  backgroundColor: Theme.of(context)\n                                      .colorScheme\n                                      .primary, // 设置按钮背景色\n                                ),\n                                onPressed: () => _loginPageCtr.loginInByCode(),\n                                child: const Text('确认登录'),\n                              )\n                            ],\n                          ),\n                        ],\n                      ),\n                    ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/main/controller.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/common.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport '../../models/common/dynamic_badge_mode.dart';\nimport '../../models/common/nav_bar_config.dart';\n\nclass MainController extends GetxController {\n  List<Widget> pages = <Widget>[];\n  List<int> pagesIds = <int>[];\n  RxList navigationBars = [].obs;\n  late List defaultNavTabs;\n  late List<int> navBarSort;\n  final StreamController<bool> bottomBarStream =\n      StreamController<bool>.broadcast();\n  Box setting = GStrorage.setting;\n  DateTime? _lastPressedAt;\n  late bool hideTabBar;\n  late PageController pageController;\n  int selectedIndex = 0;\n  Box userInfoCache = GStrorage.userInfo;\n  RxBool userLogin = false.obs;\n  late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;\n  late bool enableGradientBg;\n  bool imgPreviewStatus = false;\n\n  @override\n  void onInit() {\n    super.onInit();\n    if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) {\n      Utils.checkUpdata();\n    }\n    hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: false);\n\n    var userInfo = userInfoCache.get('userInfoCache');\n    userLogin.value = userInfo != null;\n    dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(\n        SettingBoxKey.dynamicBadgeMode,\n        defaultValue: DynamicBadgeMode.number.code)];\n    setNavBarConfig();\n    if (dynamicBadgeType.value != DynamicBadgeMode.hidden &&\n        pagesIds.contains(2)) {\n      getUnreadDynamic();\n    }\n    enableGradientBg =\n        setting.get(SettingBoxKey.enableGradientBg, defaultValue: true);\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (_lastPressedAt == null ||\n        DateTime.now().difference(_lastPressedAt!) >\n            const Duration(seconds: 2)) {\n      // 两次点击时间间隔超过2秒，重新记录时间戳\n      _lastPressedAt = DateTime.now();\n      if (selectedIndex != 0) {\n        pageController.jumpTo(0);\n      }\n      SmartDialog.showToast(\"再按一次退出Pili\");\n      return; // 不退出应用\n    }\n    SystemNavigator.pop(); // 退出应用\n  }\n\n  void getUnreadDynamic() async {\n    if (!userLogin.value) {\n      return;\n    }\n    int dynamicItemIndex =\n        navigationBars.indexWhere((item) => item['label'] == \"动态\");\n    var res = await CommonHttp.unReadDynamic();\n    var data = res['data'];\n    if (dynamicItemIndex != -1) {\n      navigationBars[dynamicItemIndex]['count'] =\n          data == null ? 0 : data.length; // 修改 count 属性为新的值\n    }\n    navigationBars.refresh();\n  }\n\n  void clearUnread() async {\n    int dynamicItemIndex =\n        navigationBars.indexWhere((item) => item['label'] == \"动态\");\n    if (dynamicItemIndex != -1) {\n      navigationBars[dynamicItemIndex]['count'] = 0; // 修改 count 属性为新的值\n    }\n    navigationBars.refresh();\n  }\n\n  void setNavBarConfig() async {\n    defaultNavTabs = [...defaultNavigationBars];\n    navBarSort =\n        setting.get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]);\n    defaultNavTabs.retainWhere((item) => navBarSort.contains(item['id']));\n    defaultNavTabs.sort((a, b) =>\n        navBarSort.indexOf(a['id']).compareTo(navBarSort.indexOf(b['id'])));\n    navigationBars.value = defaultNavTabs;\n    int defaultHomePage =\n        setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;\n    int defaultIndex =\n        navigationBars.indexWhere((item) => item['id'] == defaultHomePage);\n    // 如果找不到匹配项，默认索引设置为0或其他合适的值\n    selectedIndex = defaultIndex != -1 ? defaultIndex : 0;\n    pages = navigationBars.map<Widget>((e) => e['page']).toList();\n    pagesIds = navigationBars.map<int>((e) => e['id']).toList();\n  }\n\n  @override\n  void onClose() {\n    bottomBarStream.close();\n    super.onClose();\n  }\n}\n"
  },
  {
    "path": "lib/pages/main/index.dart",
    "content": "library main;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/main/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/dynamic_badge_mode.dart';\nimport 'package:pilipala/pages/dynamics/index.dart';\nimport 'package:pilipala/pages/home/index.dart';\nimport 'package:pilipala/pages/media/index.dart';\nimport 'package:pilipala/pages/rank/index.dart';\nimport 'package:pilipala/utils/event_bus.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport './controller.dart';\n\nclass MainApp extends StatefulWidget {\n  const MainApp({super.key});\n\n  @override\n  State<MainApp> createState() => _MainAppState();\n}\n\nclass _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {\n  final MainController _mainController = Get.put(MainController());\n  late HomeController _homeController;\n  RankController? _rankController;\n  DynamicsController? _dynamicController;\n  MediaController? _mediaController;\n\n  int? _lastSelectTime; //上次点击时间\n  Box setting = GStrorage.setting;\n  late bool enableMYBar;\n\n  @override\n  void initState() {\n    super.initState();\n    _lastSelectTime = DateTime.now().millisecondsSinceEpoch;\n    _mainController.pageController =\n        PageController(initialPage: _mainController.selectedIndex);\n    enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);\n    controllerInit();\n  }\n\n  void setIndex(int value) async {\n    feedBack();\n    _mainController.pageController.jumpToPage(value);\n    var currentPage = _mainController.pages[value];\n    if (currentPage is HomePage) {\n      if (_homeController.flag) {\n        // 单击返回顶部 双击并刷新\n        if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {\n          _homeController.onRefresh();\n        } else {\n          _homeController.animateToTop();\n        }\n        _lastSelectTime = DateTime.now().millisecondsSinceEpoch;\n      }\n      _homeController.flag = true;\n    } else {\n      _homeController.flag = false;\n    }\n\n    if (currentPage is RankPage) {\n      if (_rankController!.flag) {\n        // 单击返回顶部 双击并刷新\n        if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {\n          _rankController!.onRefresh();\n        } else {\n          _rankController!.animateToTop();\n        }\n        _lastSelectTime = DateTime.now().millisecondsSinceEpoch;\n      }\n      _rankController!.flag = true;\n    } else {\n      _rankController?.flag = false;\n    }\n\n    if (currentPage is DynamicsPage) {\n      if (_dynamicController!.flag) {\n        // 单击返回顶部 双击并刷新\n        if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {\n          _dynamicController!.onRefresh();\n        } else {\n          _dynamicController!.animateToTop();\n        }\n        _lastSelectTime = DateTime.now().millisecondsSinceEpoch;\n      }\n      _dynamicController!.flag = true;\n      _mainController.clearUnread();\n    } else {\n      _dynamicController?.flag = false;\n    }\n\n    if (currentPage is MediaPage) {\n      _mediaController!.queryFavFolder();\n    }\n  }\n\n  void controllerInit() {\n    _homeController = Get.put(HomeController());\n    if (_mainController.pagesIds.contains(1)) {\n      _rankController = Get.put(RankController());\n    }\n    if (_mainController.pagesIds.contains(2)) {\n      _dynamicController = Get.put(DynamicsController());\n    }\n    if (_mainController.pagesIds.contains(3)) {\n      _mediaController = Get.put(MediaController());\n    }\n  }\n\n  @override\n  void dispose() async {\n    await GStrorage.close();\n    EventBus().off(EventName.loginEvent);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Box localCache = GStrorage.localCache;\n    double statusBarHeight = MediaQuery.of(context).padding.top;\n    double sheetHeight = MediaQuery.sizeOf(context).height -\n        MediaQuery.of(context).padding.top -\n        MediaQuery.sizeOf(context).width * 9 / 16;\n    localCache.put('sheetHeight', sheetHeight);\n    localCache.put('statusBarHeight', statusBarHeight);\n    return PopScope(\n      canPop: false,\n      onPopInvoked: (bool didPop) async {\n        _mainController.onBackPressed(context);\n      },\n      child: Scaffold(\n        extendBody: true,\n        body: Stack(\n          children: [\n            if (_mainController.enableGradientBg)\n              Align(\n                alignment: Alignment.topLeft,\n                child: Opacity(\n                  opacity: Theme.of(context).brightness == Brightness.dark\n                      ? 0.3\n                      : 0.6,\n                  child: Container(\n                    width: MediaQuery.of(context).size.width,\n                    height: MediaQuery.of(context).size.height,\n                    decoration: BoxDecoration(\n                      gradient: LinearGradient(\n                          colors: [\n                            Theme.of(context)\n                                .colorScheme\n                                .primary\n                                .withOpacity(0.7),\n                            Theme.of(context).colorScheme.surface,\n                            Theme.of(context)\n                                .colorScheme\n                                .surface\n                                .withOpacity(0.3),\n                          ],\n                          begin: Alignment.topCenter,\n                          end: Alignment.bottomCenter,\n                          stops: const [0.1, 0.3, 5]),\n                    ),\n                  ),\n                ),\n              ),\n            PageView(\n              physics: const NeverScrollableScrollPhysics(),\n              controller: _mainController.pageController,\n              onPageChanged: (index) {\n                _mainController.selectedIndex = index;\n                setState(() {});\n              },\n              children: _mainController.pages,\n            ),\n          ],\n        ),\n        bottomNavigationBar: _mainController.navigationBars.length > 1\n            ? StreamBuilder(\n                stream: _mainController.hideTabBar\n                    ? _mainController.bottomBarStream.stream.distinct()\n                    : StreamController<bool>.broadcast().stream,\n                initialData: true,\n                builder: (context, AsyncSnapshot snapshot) {\n                  return AnimatedSlide(\n                    curve: Curves.easeInOutCubicEmphasized,\n                    duration: const Duration(milliseconds: 500),\n                    offset: Offset(0, snapshot.data ? 0 : 1),\n                    child: enableMYBar\n                        ? Obx(\n                            () => NavigationBar(\n                              onDestinationSelected: (value) => setIndex(value),\n                              selectedIndex: _mainController.selectedIndex,\n                              destinations: <Widget>[\n                                ..._mainController.navigationBars.map((e) {\n                                  return NavigationDestination(\n                                    icon: Badge(\n                                      label: _mainController\n                                                  .dynamicBadgeType.value ==\n                                              DynamicBadgeMode.number\n                                          ? Text(e['count'].toString())\n                                          : null,\n                                      padding:\n                                          const EdgeInsets.fromLTRB(6, 0, 6, 0),\n                                      isLabelVisible: _mainController\n                                                  .dynamicBadgeType.value !=\n                                              DynamicBadgeMode.hidden &&\n                                          e['count'] > 0,\n                                      child: e['icon'],\n                                    ),\n                                    selectedIcon: e['selectIcon'],\n                                    label: e['label'],\n                                  );\n                                }).toList(),\n                              ],\n                            ),\n                          )\n                        : Obx(\n                            () => BottomNavigationBar(\n                              currentIndex: _mainController.selectedIndex,\n                              type: BottomNavigationBarType.fixed,\n                              onTap: (value) => setIndex(value),\n                              iconSize: 16,\n                              selectedFontSize: 12,\n                              unselectedFontSize: 12,\n                              items: [\n                                ..._mainController.navigationBars.map((e) {\n                                  return BottomNavigationBarItem(\n                                    icon: Badge(\n                                      label: _mainController\n                                                  .dynamicBadgeType.value ==\n                                              DynamicBadgeMode.number\n                                          ? Text(e['count'].toString())\n                                          : null,\n                                      padding:\n                                          const EdgeInsets.fromLTRB(6, 0, 6, 0),\n                                      isLabelVisible: _mainController\n                                                  .dynamicBadgeType.value !=\n                                              DynamicBadgeMode.hidden &&\n                                          e['count'] > 0,\n                                      child: e['icon'],\n                                    ),\n                                    activeIcon: e['selectIcon'],\n                                    label: e['label'],\n                                  );\n                                }).toList(),\n                              ],\n                            ),\n                          ),\n                  );\n                },\n              )\n            : null,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/media/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/user/fav_folder.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass MediaController extends GetxController {\n  Rx<FavFolderData> favFolderData = FavFolderData().obs;\n  Box userInfoCache = GStrorage.userInfo;\n  RxBool userLogin = false.obs;\n  List list = [\n    {\n      'icon': Icons.file_download_outlined,\n      'title': '离线缓存',\n      'onTap': () {\n        SmartDialog.showToast('功能开发中');\n      },\n    },\n    {\n      'icon': Icons.history,\n      'title': '观看记录',\n      'onTap': () => Get.toNamed('/history'),\n    },\n    {\n      'icon': Icons.star_border,\n      'title': '我的收藏',\n      'onTap': () => Get.toNamed('/fav'),\n    },\n    {\n      'icon': Icons.subscriptions_outlined,\n      'title': '我的订阅',\n      'onTap': () => Get.toNamed('/subscription'),\n    },\n    {\n      'icon': Icons.watch_later_outlined,\n      'title': '稍后再看',\n      'onTap': () => Get.toNamed('/later'),\n    },\n  ];\n  var userInfo;\n  int? mid;\n  final ScrollController scrollController = ScrollController();\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n    userLogin.value = userInfo != null;\n  }\n\n  Future<dynamic> queryFavFolder() async {\n    if (!userLogin.value) {\n      return {'status': false, 'data': [], 'msg': '未登录'};\n    }\n    var res = await await UserHttp.userfavFolder(\n      pn: 1,\n      ps: 5,\n      mid: mid ?? GStrorage.userInfo.get('userInfoCache').mid,\n    );\n    favFolderData.value = res['data'];\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/media/index.dart",
    "content": "library media;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/media/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/user/fav_folder.dart';\nimport 'package:pilipala/pages/media/index.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass MediaPage extends StatefulWidget {\n  const MediaPage({super.key});\n\n  @override\n  State<MediaPage> createState() => _MediaPageState();\n}\n\nclass _MediaPageState extends State<MediaPage>\n    with AutomaticKeepAliveClientMixin {\n  late MediaController mediaController;\n  late Future _futureBuilderFuture;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    mediaController = Get.put(MediaController());\n    _futureBuilderFuture = mediaController.queryFavFolder();\n    mediaController.userLogin.listen((status) {\n      setState(() {\n        _futureBuilderFuture = mediaController.queryFavFolder();\n      });\n    });\n  }\n\n  @override\n  void dispose() {\n    mediaController.scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    Color primary = Theme.of(context).colorScheme.primary;\n    return Scaffold(\n      appBar: AppBar(toolbarHeight: 30),\n      body: SingleChildScrollView(\n        controller: mediaController.scrollController,\n        child: Column(\n          children: [\n            ListTile(\n              leading: null,\n              title: Padding(\n                padding: const EdgeInsets.only(left: 20),\n                child: Text(\n                  '媒体库',\n                  style: TextStyle(\n                    fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,\n                    fontWeight: FontWeight.bold,\n                  ),\n                ),\n              ),\n            ),\n            for (var i in mediaController.list) ...[\n              ListTile(\n                onTap: () => i['onTap'](),\n                dense: true,\n                leading: Padding(\n                  padding: const EdgeInsets.only(left: 15),\n                  child: Icon(\n                    i['icon'],\n                    color: primary,\n                  ),\n                ),\n                contentPadding:\n                    const EdgeInsets.only(left: 15, top: 2, bottom: 2),\n                minLeadingWidth: 0,\n                title: Text(\n                  i['title'],\n                  style: const TextStyle(fontSize: 15),\n                ),\n              ),\n            ],\n            Obx(() => mediaController.userLogin.value\n                ? favFolder(mediaController, context)\n                : const SizedBox()),\n            SizedBox(\n              height: MediaQuery.of(context).padding.bottom +\n                  kBottomNavigationBarHeight,\n            )\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget favFolder(mediaController, context) {\n    return Column(\n      children: [\n        Divider(\n          height: 35,\n          color: Theme.of(context).dividerColor.withOpacity(0.1),\n        ),\n        ListTile(\n          onTap: () => Get.toNamed('/fav'),\n          leading: null,\n          dense: true,\n          title: Padding(\n            padding: const EdgeInsets.only(left: 10),\n            child: Obx(\n              () => Text.rich(\n                TextSpan(\n                  children: [\n                    TextSpan(\n                      text: '收藏夹 ',\n                      style: TextStyle(\n                          fontSize:\n                              Theme.of(context).textTheme.titleMedium!.fontSize,\n                          fontWeight: FontWeight.bold),\n                    ),\n                    if (mediaController.favFolderData.value.count != null)\n                      TextSpan(\n                        text: mediaController.favFolderData.value.count\n                            .toString(),\n                        style: TextStyle(\n                          fontSize:\n                              Theme.of(context).textTheme.titleSmall!.fontSize,\n                          color: Theme.of(context).colorScheme.primary,\n                        ),\n                      ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n          trailing: IconButton(\n            onPressed: () {\n              setState(() {\n                _futureBuilderFuture = mediaController.queryFavFolder();\n              });\n            },\n            icon: const Icon(\n              Icons.refresh,\n              size: 20,\n            ),\n          ),\n        ),\n        // const SizedBox(height: 10),\n        SizedBox(\n          width: double.infinity,\n          height: MediaQuery.textScalerOf(context).scale(200),\n          child: FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  if (snapshot.data == null) {\n                    return const SizedBox();\n                  }\n                  Map data = snapshot.data as Map;\n                  if (data['status']) {\n                    List favFolderList =\n                        mediaController.favFolderData.value.list!;\n                    int favFolderCount =\n                        mediaController.favFolderData.value.count!;\n                    bool flag = favFolderCount > favFolderList.length;\n                    return Obx(() => ListView.builder(\n                          itemCount:\n                              mediaController.favFolderData.value.list!.length +\n                                  (flag ? 1 : 0),\n                          itemBuilder: (context, index) {\n                            if (flag && index == favFolderList.length) {\n                              return Padding(\n                                  padding: const EdgeInsets.only(\n                                      right: 14, bottom: 35),\n                                  child: Center(\n                                    child: IconButton(\n                                      style: ButtonStyle(\n                                        padding: MaterialStateProperty.all(\n                                            EdgeInsets.zero),\n                                        backgroundColor:\n                                            MaterialStateProperty.resolveWith(\n                                                (states) {\n                                          return Theme.of(context)\n                                              .colorScheme\n                                              .primaryContainer\n                                              .withOpacity(0.5);\n                                        }),\n                                      ),\n                                      onPressed: () => Get.toNamed('/fav'),\n                                      icon: Icon(\n                                        Icons.arrow_forward_ios,\n                                        size: 18,\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .primary,\n                                      ),\n                                    ),\n                                  ));\n                            } else {\n                              return FavFolderItem(\n                                  item: mediaController\n                                      .favFolderData.value.list![index],\n                                  index: index);\n                            }\n                          },\n                          scrollDirection: Axis.horizontal,\n                        ));\n                  } else {\n                    return SizedBox(\n                      height: 160,\n                      child: Center(child: Text(data['msg'])),\n                    );\n                  }\n                } else {\n                  // 骨架屏\n                  return const SizedBox();\n                }\n              }),\n        ),\n      ],\n    );\n  }\n}\n\nclass FavFolderItem extends StatelessWidget {\n  const FavFolderItem({super.key, this.item, this.index});\n  final FavFolderItemData? item;\n  final int? index;\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(item!.fid);\n\n    return Container(\n      margin: EdgeInsets.only(left: index == 0 ? 20 : 0, right: 14),\n      child: GestureDetector(\n        onTap: () => Get.toNamed('/favDetail', arguments: item, parameters: {\n          'mediaId': item!.id.toString(),\n          'heroTag': heroTag,\n          'isOwner': '1',\n        }),\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.start,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            const SizedBox(height: 12),\n            Container(\n              width: 180,\n              height: 110,\n              margin: const EdgeInsets.only(bottom: 8),\n              clipBehavior: Clip.hardEdge,\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(12),\n                color: Theme.of(context).colorScheme.onInverseSurface,\n                boxShadow: [\n                  BoxShadow(\n                    color: Theme.of(context).colorScheme.onInverseSurface,\n                    offset: const Offset(4, -12), // 阴影与容器的距离\n                    blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。\n                    spreadRadius: 0.0, // 在应用模糊之前，框应该膨胀的量。\n                  ),\n                ],\n              ),\n              child: LayoutBuilder(\n                builder: (context, BoxConstraints box) {\n                  return Hero(\n                    tag: heroTag,\n                    child: NetworkImgLayer(\n                      src: item!.cover,\n                      width: box.maxWidth,\n                      height: box.maxHeight,\n                    ),\n                  );\n                },\n              ),\n            ),\n            Text(\n              ' ${item!.title}',\n              overflow: TextOverflow.fade,\n              maxLines: 1,\n            ),\n            Text(\n              ' 共${item!.mediaCount}条视频',\n              style: Theme.of(context)\n                  .textTheme\n                  .labelSmall!\n                  .copyWith(color: Theme.of(context).colorScheme.outline),\n            )\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/member/archive.dart';\nimport 'package:pilipala/models/member/coin.dart';\nimport 'package:pilipala/models/member/info.dart';\nimport 'package:pilipala/models/member/like.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:share_plus/share_plus.dart';\n\nclass MemberController extends GetxController {\n  late int mid;\n  Rx<MemberInfoModel> memberInfo = MemberInfoModel().obs;\n  late Map userStat;\n  RxString face = ''.obs;\n  String? heroTag;\n  Box userInfoCache = GStrorage.userInfo;\n  late int ownerMid;\n  // 投稿列表\n  RxList<VListItemModel>? archiveList = <VListItemModel>[].obs;\n  dynamic userInfo;\n  RxInt attribute = (-1).obs;\n  RxString attributeText = '关注'.obs;\n  RxList<MemberCoinsDataModel> recentCoinsList = <MemberCoinsDataModel>[].obs;\n  RxList<MemberLikeDataModel> recentLikeList = <MemberLikeDataModel>[].obs;\n  RxBool isOwner = false.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    mid = int.parse(Get.parameters['mid']!);\n    userInfo = userInfoCache.get('userInfoCache');\n    ownerMid = userInfo != null ? userInfo.mid : -1;\n    isOwner.value = mid == ownerMid;\n    face.value = Get.arguments['face'] ?? '';\n    heroTag = Get.arguments['heroTag'] ?? '';\n    relationSearch();\n  }\n\n  // 获取用户信息\n  Future<Map<String, dynamic>> getInfo() async {\n    await getMemberStat();\n    await getMemberView();\n    var res = await MemberHttp.memberInfo(mid: mid);\n    if (res['status']) {\n      memberInfo.value = res['data'];\n      face.value = res['data'].face;\n    }\n    return res;\n  }\n\n  // 获取用户状态\n  Future<Map<String, dynamic>> getMemberStat() async {\n    var res = await MemberHttp.memberStat(mid: mid);\n    if (res['status']) {\n      userStat = res['data'];\n    }\n    return res;\n  }\n\n  // 获取用户播放数 获赞数\n  Future<Map<String, dynamic>> getMemberView() async {\n    var res = await MemberHttp.memberView(mid: mid);\n    if (res['status']) {\n      userStat.addAll(res['data']);\n    }\n    return res;\n  }\n\n  // 关注/取关up\n  Future actionRelationMod() async {\n    if (userInfo == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    if (attribute.value == 128) {\n      blockUser();\n      return;\n    }\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: Text(memberInfo.value.isFollowed! ? '取消关注UP主?' : '关注UP主?'),\n          actions: [\n            TextButton(\n              onPressed: () => SmartDialog.dismiss(),\n              child: Text(\n                '点错了',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                await VideoHttp.relationMod(\n                  mid: mid,\n                  act: memberInfo.value.isFollowed! ? 2 : 1,\n                  reSrc: 11,\n                );\n                memberInfo.value.isFollowed = !memberInfo.value.isFollowed!;\n                relationSearch();\n                SmartDialog.dismiss();\n                memberInfo.update((val) {});\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  // 关系查询\n  Future relationSearch() async {\n    if (userInfo == null) return;\n    if (mid == ownerMid) return;\n    var res = await UserHttp.hasFollow(mid);\n    if (res['status']) {\n      attribute.value = res['data']['attribute'];\n      switch (attribute.value) {\n        case 1:\n          attributeText.value = '悄悄关注';\n          break;\n        case 2:\n          attributeText.value = '已关注';\n          break;\n        case 6:\n          attributeText.value = '已互关';\n          break;\n        case 128:\n          attributeText.value = '已拉黑';\n          break;\n        default:\n          attributeText.value = '关注';\n      }\n      if (res['data']['special'] == 1) {\n        attributeText.value += 'SP';\n      }\n    }\n  }\n\n  // 拉黑用户\n  Future blockUser() async {\n    if (userInfo == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: Text(attribute.value != 128 ? '确定拉黑UP主?' : '从黑名单移除UP主'),\n          actions: [\n            TextButton(\n              onPressed: () => SmartDialog.dismiss(),\n              child: Text(\n                '点错了',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                var res = await VideoHttp.relationMod(\n                  mid: mid,\n                  act: attribute.value != 128 ? 5 : 6,\n                  reSrc: 11,\n                );\n                SmartDialog.dismiss();\n                if (res['status']) {\n                  attribute.value = attribute.value != 128 ? 128 : 0;\n                  attributeText.value = attribute.value == 128 ? '已拉黑' : '关注';\n                  memberInfo.value.isFollowed = false;\n                  relationSearch();\n                  memberInfo.update((val) {});\n                }\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  void shareUser() {\n    Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid');\n  }\n\n  // 请求合集\n  Future getMemberSeasons() async {\n    if (userInfo == null) return;\n    var res = await MemberHttp.getMemberSeasons(mid, 1, 10);\n    if (!res['status']) {\n      SmartDialog.showToast(\"用户合集请求异常：${res['msg']}\");\n    } else {\n      // 只取前四个专栏\n      res['data'].seasonsList.map((e) {\n        e.archives =\n            e.archives!.length > 4 ? e.archives!.sublist(0, 4) : e.archives!;\n      }).toList();\n    }\n    return res;\n  }\n\n  // 请求投币视频\n  Future getRecentCoinVideo() async {\n    if (userInfo == null) return;\n    var res = await MemberHttp.getRecentCoinVideo(mid: mid);\n    recentCoinsList.value = res['data'];\n    return res;\n  }\n\n  // 请求点赞视频\n  Future getRecentLikeVideo() async {\n    if (userInfo == null) return;\n    var res = await MemberHttp.getRecentLikeVideo(mid: mid);\n    recentLikeList.value = res['data'];\n    return res;\n  }\n\n  // 跳转查看动态\n  void pushDynamicsPage() => Get.toNamed('/memberDynamics?mid=$mid');\n\n  // 跳转查看投稿\n  void pushArchivesPage() => Get.toNamed('/memberArchive?mid=$mid');\n\n  // 跳转查看专栏\n  void pushSeasonsPage() {}\n  // 跳转查看最近投币\n  void pushRecentCoinsPage() async {\n    if (recentCoinsList.isNotEmpty) {}\n  }\n\n  void pushfavPage() => Get.toNamed('/fav?mid=$mid');\n  // 跳转图文专栏\n  void pushArticlePage() => Get.toNamed('/memberArticle?mid=$mid');\n}\n"
  },
  {
    "path": "lib/pages/member/index.dart",
    "content": "library member;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/member/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/pages/member/index.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'widgets/conis.dart';\nimport 'widgets/like.dart';\nimport 'widgets/profile.dart';\nimport 'widgets/seasons.dart';\n\nclass MemberPage extends StatefulWidget {\n  const MemberPage({super.key});\n\n  @override\n  State<MemberPage> createState() => _MemberPageState();\n}\n\nclass _MemberPageState extends State<MemberPage>\n    with SingleTickerProviderStateMixin {\n  late String heroTag;\n  late MemberController _memberController;\n  late Future _futureBuilderFuture;\n  late Future _memberSeasonsFuture;\n  late Future _memberCoinsFuture;\n  late Future _memberLikeFuture;\n  final ScrollController _extendNestCtr = ScrollController();\n  final StreamController<bool> appbarStream =\n      StreamController<bool>.broadcast();\n  late int mid;\n\n  @override\n  void initState() {\n    super.initState();\n    mid = int.parse(Get.parameters['mid']!);\n    heroTag = Get.arguments['heroTag'] ?? Utils.makeHeroTag(mid);\n    _memberController = Get.put(MemberController(), tag: heroTag);\n    _futureBuilderFuture = _memberController.getInfo();\n    _memberSeasonsFuture = _memberController.getMemberSeasons();\n    _memberCoinsFuture = _memberController.getRecentCoinVideo();\n    _memberLikeFuture = _memberController.getRecentLikeVideo();\n    _extendNestCtr.addListener(\n      () {\n        final double offset = _extendNestCtr.position.pixels;\n        if (offset > 100) {\n          appbarStream.add(true);\n        } else {\n          appbarStream.add(false);\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _extendNestCtr.removeListener(() {});\n    appbarStream.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      primary: true,\n      body: Column(\n        children: [\n          AppBar(\n            title: StreamBuilder(\n              stream: appbarStream.stream.distinct(),\n              initialData: false,\n              builder: (BuildContext context, AsyncSnapshot snapshot) {\n                return AnimatedOpacity(\n                  opacity: snapshot.data ? 1 : 0,\n                  curve: Curves.easeOut,\n                  duration: const Duration(milliseconds: 500),\n                  child: Row(\n                    children: [\n                      Row(\n                        children: [\n                          Obx(\n                            () => NetworkImgLayer(\n                              width: 35,\n                              height: 35,\n                              type: 'avatar',\n                              src: _memberController.face.value,\n                            ),\n                          ),\n                          const SizedBox(width: 10),\n                          Obx(\n                            () => Text(\n                              _memberController.memberInfo.value.name ?? '',\n                              style: TextStyle(\n                                  color:\n                                      Theme.of(context).colorScheme.onSurface,\n                                  fontSize: 14),\n                            ),\n                          ),\n                        ],\n                      )\n                    ],\n                  ),\n                );\n              },\n            ),\n            actions: [\n              IconButton(\n                onPressed: () => Get.toNamed(\n                    '/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'),\n                icon: const Icon(Icons.search_outlined),\n              ),\n              PopupMenuButton(\n                icon: const Icon(Icons.more_vert),\n                itemBuilder: (BuildContext context) => <PopupMenuEntry>[\n                  if (_memberController.ownerMid != _memberController.mid) ...[\n                    PopupMenuItem(\n                      onTap: () => _memberController.blockUser(),\n                      child: Row(\n                        mainAxisSize: MainAxisSize.min,\n                        children: [\n                          const Icon(Icons.block, size: 19),\n                          const SizedBox(width: 10),\n                          Text(_memberController.attribute.value != 128\n                              ? '加入黑名单'\n                              : '移除黑名单'),\n                        ],\n                      ),\n                    )\n                  ],\n                  PopupMenuItem(\n                    onTap: () => _memberController.shareUser(),\n                    child: Row(\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        const Icon(Icons.share_outlined, size: 19),\n                        const SizedBox(width: 10),\n                        Text(_memberController.ownerMid != _memberController.mid\n                            ? '分享UP主'\n                            : '分享我的主页'),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n              const SizedBox(width: 4),\n            ],\n          ),\n          Expanded(\n            child: SingleChildScrollView(\n              controller: _extendNestCtr,\n              child: Padding(\n                padding: EdgeInsets.only(\n                  bottom: MediaQuery.of(context).padding.bottom + 20,\n                ),\n                child: Column(\n                  children: [\n                    profileWidget(),\n\n                    /// 动态链接\n                    Obx(\n                      () => ListTile(\n                        onTap: _memberController.pushDynamicsPage,\n                        title: Text(\n                            '${_memberController.isOwner.value ? '我' : 'Ta'}的动态'),\n                        trailing:\n                            const Icon(Icons.arrow_forward_outlined, size: 19),\n                      ),\n                    ),\n\n                    /// 视频\n                    Obx(\n                      () => ListTile(\n                        onTap: _memberController.pushArchivesPage,\n                        title: Text(\n                            '${_memberController.isOwner.value ? '我' : 'Ta'}的投稿'),\n                        trailing:\n                            const Icon(Icons.arrow_forward_outlined, size: 19),\n                      ),\n                    ),\n\n                    /// 他的收藏夹\n                    Obx(\n                      () => ListTile(\n                        onTap: _memberController.pushfavPage,\n                        title: Text(\n                            '${_memberController.isOwner.value ? '我' : 'Ta'}的收藏'),\n                        trailing:\n                            const Icon(Icons.arrow_forward_outlined, size: 19),\n                      ),\n                    ),\n\n                    /// 专栏\n                    Obx(\n                      () => ListTile(\n                        onTap: _memberController.pushArticlePage,\n                        title: Text(\n                            '${_memberController.isOwner.value ? '我' : 'Ta'}的专栏'),\n                        trailing:\n                            const Icon(Icons.arrow_forward_outlined, size: 19),\n                      ),\n                    ),\n\n                    /// 合集\n                    Obx(\n                      () => ListTile(\n                          title: Text(\n                              '${_memberController.isOwner.value ? '我' : 'Ta'}的合集')),\n                    ),\n                    MediaQuery.removePadding(\n                      removeTop: true,\n                      removeBottom: true,\n                      context: context,\n                      child: FutureBuilder(\n                        future: _memberSeasonsFuture,\n                        builder: (context, snapshot) {\n                          if (snapshot.connectionState ==\n                              ConnectionState.done) {\n                            if (snapshot.data == null) {\n                              return const SizedBox();\n                            }\n                            if (snapshot.data['status']) {\n                              Map data = snapshot.data as Map;\n                              if (data['data'].seasonsList.isEmpty) {\n                                return commenWidget('用户没有设置合集');\n                              } else {\n                                return MemberSeasonsPanel(data: data['data']);\n                              }\n                            } else {\n                              // 请求错误\n                              return const SizedBox();\n                            }\n                          } else {\n                            return const SizedBox();\n                          }\n                        },\n                      ),\n                    ),\n\n                    /// 追番\n                    /// 最近投币\n                    Obx(\n                      () => _memberController.recentCoinsList.isNotEmpty\n                          ? const ListTile(title: Text('最近投币的视频'))\n                          : const SizedBox(),\n                    ),\n                    MediaQuery.removePadding(\n                      removeTop: true,\n                      removeBottom: true,\n                      context: context,\n                      child: Padding(\n                        padding: const EdgeInsets.only(\n                          left: StyleString.safeSpace,\n                          right: StyleString.safeSpace,\n                        ),\n                        child: FutureBuilder(\n                          future: _memberCoinsFuture,\n                          builder: (context, snapshot) {\n                            if (snapshot.connectionState ==\n                                ConnectionState.done) {\n                              if (snapshot.data == null) {\n                                return const SizedBox();\n                              }\n                              if (snapshot.data['status']) {\n                                Map data = snapshot.data as Map;\n                                return MemberCoinsPanel(data: data['data']);\n                              } else {\n                                // 请求错误\n                                return const SizedBox();\n                              }\n                            } else {\n                              return const SizedBox();\n                            }\n                          },\n                        ),\n                      ),\n                    ),\n\n                    /// 最近点赞\n                    Obx(\n                      () => _memberController.recentLikeList.isNotEmpty\n                          ? const ListTile(title: Text('最近点赞的视频'))\n                          : const SizedBox(),\n                    ),\n                    MediaQuery.removePadding(\n                      removeTop: true,\n                      removeBottom: true,\n                      context: context,\n                      child: Padding(\n                        padding: const EdgeInsets.only(\n                          left: StyleString.safeSpace,\n                          right: StyleString.safeSpace,\n                        ),\n                        child: FutureBuilder(\n                          future: _memberLikeFuture,\n                          builder: (context, snapshot) {\n                            if (snapshot.connectionState ==\n                                ConnectionState.done) {\n                              if (snapshot.data == null) {\n                                return const SizedBox();\n                              }\n                              if (snapshot.data['status']) {\n                                Map data = snapshot.data as Map;\n                                return MemberLikePanel(data: data['data']);\n                              } else {\n                                // 请求错误\n                                return const SizedBox();\n                              }\n                            } else {\n                              return const SizedBox();\n                            }\n                          },\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget profileWidget() {\n    return Padding(\n      padding: const EdgeInsets.only(left: 18, right: 18, bottom: 20),\n      child: FutureBuilder(\n        future: _futureBuilderFuture,\n        builder: (context, snapshot) {\n          if (snapshot.connectionState == ConnectionState.done) {\n            Map? data = snapshot.data;\n            if (data != null && data['status']) {\n              return Obx(\n                () => Stack(\n                  alignment: AlignmentDirectional.center,\n                  children: [\n                    Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        ProfilePanel(ctr: _memberController),\n                        const SizedBox(height: 20),\n                        Row(\n                          children: [\n                            Flexible(\n                                child: Text(\n                              _memberController.memberInfo.value.name!,\n                              maxLines: 1,\n                              overflow: TextOverflow.ellipsis,\n                              style: Theme.of(context)\n                                  .textTheme\n                                  .titleMedium!\n                                  .copyWith(\n                                      fontWeight: FontWeight.bold,\n                                      color: _memberController.memberInfo.value\n                                                  .vip!.nicknameColor !=\n                                              null\n                                          ? Color(_memberController.memberInfo\n                                              .value.vip!.nicknameColor!)\n                                          : null),\n                            )),\n                            const SizedBox(width: 2),\n                            if (_memberController.memberInfo.value.sex == '女')\n                              const Icon(\n                                FontAwesomeIcons.venus,\n                                size: 14,\n                                color: Colors.pink,\n                              ),\n                            if (_memberController.memberInfo.value.sex == '男')\n                              const Icon(\n                                FontAwesomeIcons.mars,\n                                size: 14,\n                                color: Colors.blue,\n                              ),\n                            const SizedBox(width: 4),\n                            Image.asset(\n                              'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',\n                              height: 11,\n                            ),\n                            const SizedBox(width: 6),\n                            if (_memberController\n                                        .memberInfo.value.vip!.status ==\n                                    1 &&\n                                _memberController.memberInfo.value.vip!\n                                        .label!['img_label_uri_hans'] !=\n                                    '') ...[\n                              Image.network(\n                                _memberController.memberInfo.value.vip!\n                                    .label!['img_label_uri_hans'],\n                                height: 20,\n                              ),\n                            ] else if (_memberController\n                                        .memberInfo.value.vip!.status ==\n                                    1 &&\n                                _memberController.memberInfo.value.vip!\n                                        .label!['img_label_uri_hans_static'] !=\n                                    '') ...[\n                              Image.network(\n                                _memberController.memberInfo.value.vip!\n                                    .label!['img_label_uri_hans_static'],\n                                height: 20,\n                              ),\n                            ]\n                          ],\n                        ),\n                        if (_memberController\n                                .memberInfo.value.official!['title'] !=\n                            '') ...[\n                          const SizedBox(height: 6),\n                          Text.rich(\n                            maxLines: 2,\n                            TextSpan(\n                              text: _memberController\n                                          .memberInfo.value.official!['role'] ==\n                                      1\n                                  ? '个人认证：'\n                                  : '企业认证：',\n                              style: TextStyle(\n                                color: Theme.of(context).colorScheme.primary,\n                              ),\n                              children: [\n                                TextSpan(\n                                  text: _memberController\n                                      .memberInfo.value.official!['title'],\n                                ),\n                              ],\n                            ),\n                            softWrap: true,\n                          ),\n                        ],\n                        const SizedBox(height: 6),\n                        if (_memberController.memberInfo.value.sign != '')\n                          SelectableText(\n                            _memberController.memberInfo.value.sign!,\n                          ),\n                      ],\n                    ),\n                  ],\n                ),\n              );\n            } else {\n              return const SizedBox();\n            }\n          } else {\n            // 骨架屏\n            return ProfilePanel(ctr: _memberController, loadingStatus: true);\n          }\n        },\n      ),\n    );\n  }\n\n  Widget commenWidget(msg) {\n    return Padding(\n      padding: const EdgeInsets.only(\n        top: 20,\n        bottom: 30,\n      ),\n      child: Center(\n        child: Text(\n          msg,\n          style: Theme.of(context)\n              .textTheme\n              .labelMedium!\n              .copyWith(color: Theme.of(context).colorScheme.outline),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member/widgets/conis.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/models/member/coin.dart';\nimport 'package:pilipala/pages/member_coin/widgets/item.dart';\n\nclass MemberCoinsPanel extends StatelessWidget {\n  final List<MemberCoinsDataModel> data;\n  const MemberCoinsPanel({super.key, required this.data});\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (context, boxConstraints) {\n        return GridView.builder(\n          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\n            crossAxisCount: 2, // Use a fixed count for GridView\n            crossAxisSpacing: StyleString.safeSpace,\n            mainAxisSpacing: StyleString.safeSpace,\n            childAspectRatio: 0.94,\n          ),\n          physics: const NeverScrollableScrollPhysics(),\n          shrinkWrap: true,\n          itemCount: data.length,\n          itemBuilder: (context, i) {\n            return MemberCoinsItem(coinItem: data[i]);\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member/widgets/like.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/models/member/like.dart';\nimport 'package:pilipala/pages/member_like/widgets/item.dart';\n\nclass MemberLikePanel extends StatelessWidget {\n  final List<MemberLikeDataModel> data;\n  const MemberLikePanel({super.key, required this.data});\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (context, boxConstraints) {\n        return GridView.builder(\n          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\n            crossAxisCount: 2, // Use a fixed count for GridView\n            crossAxisSpacing: StyleString.safeSpace,\n            mainAxisSpacing: StyleString.safeSpace,\n            childAspectRatio: 0.94,\n          ),\n          physics: const NeverScrollableScrollPhysics(),\n          shrinkWrap: true,\n          itemCount: data.length,\n          itemBuilder: (context, i) {\n            return MemberLikeItem(likeItem: data[i]);\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member/widgets/profile.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/live/item.dart';\nimport 'package:pilipala/models/member/info.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass ProfilePanel extends StatelessWidget {\n  final dynamic ctr;\n  final bool loadingStatus;\n  const ProfilePanel({\n    super.key,\n    required this.ctr,\n    this.loadingStatus = false,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    MemberInfoModel memberInfo = ctr.memberInfo.value;\n    return Builder(\n      builder: ((context) {\n        return Padding(\n          padding:\n              EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),\n          child: Row(\n            children: [\n              Hero(\n                tag: ctr.heroTag!,\n                child: Stack(\n                  children: [\n                    NetworkImgLayer(\n                      width: 90,\n                      height: 90,\n                      type: 'avatar',\n                      src: !loadingStatus ? memberInfo.face : ctr.face.value,\n                    ),\n                    if (!loadingStatus &&\n                        memberInfo.liveRoom != null &&\n                        memberInfo.liveRoom!.liveStatus == 1)\n                      Positioned(\n                        bottom: 0,\n                        left: 14,\n                        child: GestureDetector(\n                          onTap: () {\n                            LiveItemModel liveItem = LiveItemModel.fromJson({\n                              'title': memberInfo.liveRoom!.title,\n                              'uname': memberInfo.name,\n                              'face': memberInfo.face,\n                              'roomid': memberInfo.liveRoom!.roomId,\n                              'watched_show': memberInfo.liveRoom!.watchedShow,\n                            });\n                            Get.toNamed(\n                              '/liveRoom?roomid=${memberInfo.liveRoom!.roomId}',\n                              arguments: {'liveItem': liveItem},\n                            );\n                          },\n                          child: Container(\n                            padding: const EdgeInsets.fromLTRB(6, 2, 6, 2),\n                            decoration: BoxDecoration(\n                              color: Theme.of(context).colorScheme.primary,\n                              borderRadius:\n                                  const BorderRadius.all(Radius.circular(10)),\n                            ),\n                            child: Row(children: [\n                              Image.asset(\n                                'assets/images/live.gif',\n                                height: 10,\n                              ),\n                              Text(\n                                ' 直播中',\n                                style: TextStyle(\n                                    color: Colors.white,\n                                    fontSize: Theme.of(context)\n                                        .textTheme\n                                        .labelSmall!\n                                        .fontSize),\n                              )\n                            ]),\n                          ),\n                        ),\n                      )\n                  ],\n                ),\n              ),\n              const SizedBox(width: 12),\n              Expanded(\n                child: Column(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Padding(\n                      padding:\n                          const EdgeInsets.only(top: 10, left: 10, right: 10),\n                      child: Row(\n                        mainAxisSize: MainAxisSize.max,\n                        mainAxisAlignment: MainAxisAlignment.spaceAround,\n                        children: [\n                          InkWell(\n                            onTap: () {\n                              Get.toNamed(\n                                  '/follow?mid=${memberInfo.mid}&name=${memberInfo.name}');\n                            },\n                            child: Column(\n                              children: [\n                                Text(\n                                  !loadingStatus\n                                      ? ctr.userStat!['following'].toString()\n                                      : '-',\n                                  style: const TextStyle(\n                                      fontWeight: FontWeight.bold),\n                                ),\n                                Text(\n                                  '关注',\n                                  style: TextStyle(\n                                      fontSize: Theme.of(context)\n                                          .textTheme\n                                          .labelMedium!\n                                          .fontSize),\n                                )\n                              ],\n                            ),\n                          ),\n                          InkWell(\n                            onTap: () {\n                              Get.toNamed(\n                                  '/fan?mid=${memberInfo.mid}&name=${memberInfo.name}');\n                            },\n                            child: Column(\n                              children: [\n                                Text(\n                                    !loadingStatus\n                                        ? ctr.userStat!['follower'] != null\n                                            ? Utils.numFormat(\n                                                ctr.userStat!['follower'],\n                                              )\n                                            : '-'\n                                        : '-',\n                                    style: const TextStyle(\n                                        fontWeight: FontWeight.bold)),\n                                Text(\n                                  '粉丝',\n                                  style: TextStyle(\n                                      fontSize: Theme.of(context)\n                                          .textTheme\n                                          .labelMedium!\n                                          .fontSize),\n                                )\n                              ],\n                            ),\n                          ),\n                          Column(\n                            children: [\n                              Text(\n                                  !loadingStatus\n                                      ? ctr.userStat!['likes'] != null\n                                          ? Utils.numFormat(\n                                              ctr.userStat!['likes'],\n                                            )\n                                          : '-'\n                                      : '-',\n                                  style: const TextStyle(\n                                      fontWeight: FontWeight.bold)),\n                              Text(\n                                '获赞',\n                                style: TextStyle(\n                                    fontSize: Theme.of(context)\n                                        .textTheme\n                                        .labelMedium!\n                                        .fontSize),\n                              )\n                            ],\n                          ),\n                        ],\n                      ),\n                    ),\n                    const SizedBox(height: 10),\n                    if (ctr.ownerMid != ctr.mid && ctr.ownerMid != -1) ...[\n                      Row(\n                        children: [\n                          Obx(\n                            () => Expanded(\n                              child: TextButton(\n                                onPressed: () => loadingStatus\n                                    ? null\n                                    : ctr.actionRelationMod(),\n                                style: TextButton.styleFrom(\n                                  foregroundColor: ctr.attribute.value == -1\n                                      ? Colors.transparent\n                                      : ctr.attribute.value != 0\n                                          ? Theme.of(context)\n                                              .colorScheme\n                                              .outline\n                                          : Theme.of(context)\n                                              .colorScheme\n                                              .onPrimary,\n                                  backgroundColor: ctr.attribute.value != 0\n                                      ? Theme.of(context)\n                                          .colorScheme\n                                          .onInverseSurface\n                                      : Theme.of(context)\n                                          .colorScheme\n                                          .primary, // 设置按钮背景色\n                                ),\n                                child: Obx(() => Text(ctr.attributeText.value)),\n                              ),\n                            ),\n                          ),\n                          const SizedBox(width: 8),\n                          Expanded(\n                            child: TextButton(\n                              onPressed: () {\n                                Get.toNamed(\n                                  '/whisperDetail',\n                                  parameters: {\n                                    'name': memberInfo.name!,\n                                    'face': memberInfo.face!,\n                                    'mid': memberInfo.mid.toString(),\n                                    'heroTag': ctr.heroTag!,\n                                  },\n                                );\n                              },\n                              style: TextButton.styleFrom(\n                                backgroundColor: Theme.of(context)\n                                    .colorScheme\n                                    .onInverseSurface,\n                              ),\n                              child: const Text('发消息'),\n                            ),\n                          )\n                        ],\n                      )\n                    ],\n                    if (ctr.ownerMid == ctr.mid && ctr.ownerMid != -1) ...[\n                      TextButton(\n                        onPressed: () {\n                          SmartDialog.showToast('功能开发中 💪');\n                        },\n                        style: TextButton.styleFrom(\n                          padding: const EdgeInsets.only(left: 80, right: 80),\n                          foregroundColor:\n                              Theme.of(context).colorScheme.onPrimary,\n                          backgroundColor:\n                              Theme.of(context).colorScheme.primary,\n                        ),\n                        child: const Text('编辑资料'),\n                      )\n                    ],\n                    if (ctr.ownerMid == -1) ...[\n                      TextButton(\n                        onPressed: () {},\n                        style: TextButton.styleFrom(\n                          padding: const EdgeInsets.only(left: 80, right: 80),\n                          foregroundColor:\n                              Theme.of(context).colorScheme.outline,\n                          backgroundColor:\n                              Theme.of(context).colorScheme.onInverseSurface,\n                        ),\n                        child: const Text('未登录'),\n                      )\n                    ]\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member/widgets/seasons.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/models/member/seasons.dart';\nimport 'package:pilipala/pages/member_seasons/widgets/item.dart';\n\nclass MemberSeasonsPanel extends StatelessWidget {\n  final MemberSeasonsDataModel? data;\n  const MemberSeasonsPanel({super.key, this.data});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.builder(\n      shrinkWrap: true,\n      physics: const NeverScrollableScrollPhysics(),\n      itemCount: data!.seasonsList!.length,\n      itemBuilder: (context, index) {\n        MemberSeasonsList item = data!.seasonsList![index];\n        return Padding(\n          padding: const EdgeInsets.only(bottom: 12),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.start,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              ListTile(\n                onTap: () {\n                  final int category = item.meta!.category!;\n                  Map<String, String> parameters = {};\n                  if (category == 0) {\n                    parameters = {\n                      'category': '0',\n                      'mid': item.meta!.mid.toString(),\n                      'seasonId': item.meta!.seasonId.toString(),\n                      'seasonName': item.meta!.name!,\n                    };\n                  }\n                  // 2为直播回放\n                  if (category == 1 || category == 2) {\n                    parameters = {\n                      'category': '1',\n                      'mid': item.meta!.mid.toString(),\n                      'seriesId': item.meta!.seriesId.toString(),\n                      'seasonName': item.meta!.name!,\n                    };\n                  }\n                  Get.toNamed('/memberSeasons', parameters: parameters);\n                },\n                title: Text(\n                  item.meta!.name!,\n                  maxLines: 1,\n                  overflow: TextOverflow.ellipsis,\n                  style: Theme.of(context).textTheme.titleSmall!,\n                ),\n                dense: true,\n                leading: PBadge(\n                  stack: 'relative',\n                  size: 'small',\n                  text: item.meta!.total.toString(),\n                ),\n                trailing: const Icon(\n                  Icons.arrow_forward,\n                  size: 20,\n                ),\n              ),\n              const SizedBox(height: 10),\n              Padding(\n                padding: const EdgeInsets.only(\n                  left: StyleString.safeSpace,\n                  right: StyleString.safeSpace,\n                ),\n                child: LayoutBuilder(\n                  builder: (context, boxConstraints) {\n                    return GridView.builder(\n                      gridDelegate:\n                          const SliverGridDelegateWithFixedCrossAxisCount(\n                        crossAxisCount: 2, // Use a fixed count for GridView\n                        crossAxisSpacing: StyleString.safeSpace,\n                        mainAxisSpacing: StyleString.safeSpace,\n                        childAspectRatio: 0.94,\n                      ),\n                      physics: const NeverScrollableScrollPhysics(),\n                      shrinkWrap: true,\n                      itemCount: item.archives!.length,\n                      itemBuilder: (context, i) {\n                        return MemberSeasonsItem(seasonItem: item.archives![i]);\n                      },\n                    );\n                  },\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_archive/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/member/archive.dart';\n\nclass MemberArchiveController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  late int mid;\n  int pn = 1;\n  int count = 0;\n  RxMap<String, String> currentOrder = <String, String>{}.obs;\n  RxList<Map<String, String>> orderList = [\n    {'type': 'pubdate', 'label': '最新发布'},\n    {'type': 'click', 'label': '最多播放'},\n    {'type': 'stow', 'label': '最多收藏'},\n    {'type': 'charge', 'label': '充电专属'},\n  ].obs;\n  RxList<VListItemModel> archivesList = <VListItemModel>[].obs;\n  RxBool isLoading = false.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    mid = int.parse(Get.parameters['mid']!);\n    currentOrder.value = orderList.first;\n  }\n\n  // 获取用户投稿\n  Future getMemberArchive(type) async {\n    if (isLoading.value) {\n      return;\n    }\n    isLoading.value = true;\n    if (type == 'init') {\n      pn = 1;\n      archivesList.clear();\n    }\n    var res = await MemberHttp.memberArchive(\n      mid: mid,\n      pn: pn,\n      order: currentOrder['type']!,\n    );\n    if (res['status']) {\n      if (type == 'init') {\n        archivesList.value = res['data'].list.vlist;\n      }\n      if (type == 'onLoad') {\n        archivesList.addAll(res['data'].list.vlist);\n      }\n      count = res['data'].page['count'];\n      pn += 1;\n    }\n    isLoading.value = false;\n    return res;\n  }\n\n  toggleSort() async {\n    List<String> typeList = orderList.map((e) => e['type']!).toList();\n    int index = typeList.indexOf(currentOrder['type']!);\n    if (index == orderList.length - 1) {\n      currentOrder.value = orderList.first;\n    } else {\n      currentOrder.value = orderList[index + 1];\n    }\n    getMemberArchive('init');\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    getMemberArchive('onLoad');\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_archive/index.dart",
    "content": "library member_archive;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/member_archive/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/common/widgets/video_card_h.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport '../../common/widgets/http_error.dart';\nimport 'controller.dart';\n\nclass MemberArchivePage extends StatefulWidget {\n  const MemberArchivePage({super.key});\n\n  @override\n  State<MemberArchivePage> createState() => _MemberArchivePageState();\n}\n\nclass _MemberArchivePageState extends State<MemberArchivePage> {\n  late MemberArchiveController _memberArchivesController;\n  late Future _futureBuilderFuture;\n  late ScrollController scrollController;\n  late int mid;\n\n  @override\n  void initState() {\n    super.initState();\n    mid = int.parse(Get.parameters['mid']!);\n    final String heroTag = Utils.makeHeroTag(mid);\n    _memberArchivesController =\n        Get.put(MemberArchiveController(), tag: heroTag);\n    _futureBuilderFuture = _memberArchivesController.getMemberArchive('init');\n    scrollController = _memberArchivesController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle(\n              'member_archives', const Duration(milliseconds: 500), () {\n            _memberArchivesController.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        centerTitle: false,\n        title: Obx(\n          () => Text(\n              '他的投稿 - ${_memberArchivesController.currentOrder['label']}',\n              style: Theme.of(context).textTheme.titleMedium),\n        ),\n        actions: [\n          // Obx(\n          PopupMenuButton(\n            icon: const Icon(Icons.more_vert),\n            onSelected: (value) {\n              // 这里处理选择逻辑\n              _memberArchivesController.currentOrder.value = value;\n              _memberArchivesController.getMemberArchive('init');\n            },\n            itemBuilder: (BuildContext context) =>\n                _memberArchivesController.orderList.map(\n              (e) {\n                return PopupMenuItem(\n                  value: e,\n                  child: Text(e['label']!),\n                );\n              },\n            ).toList(),\n          ),\n          const SizedBox(width: 6),\n        ],\n      ),\n      body: CustomScrollView(\n        controller: _memberArchivesController.scrollController,\n        slivers: [\n          FutureBuilder(\n            future: _futureBuilderFuture,\n            builder: (BuildContext context, snapshot) {\n              if (snapshot.connectionState == ConnectionState.done) {\n                if (snapshot.data != null) {\n                  Map data = snapshot.data as Map;\n                  List list = _memberArchivesController.archivesList;\n                  if (data['status']) {\n                    return Obx(\n                      () => list.isNotEmpty\n                          ? SliverList(\n                              delegate: SliverChildBuilderDelegate(\n                                (BuildContext context, index) {\n                                  return VideoCardH(\n                                    videoItem: list[index],\n                                    showOwner: false,\n                                    showPubdate: true,\n                                    showCharge: true,\n                                  );\n                                },\n                                childCount: list.length,\n                              ),\n                            )\n                          : _memberArchivesController.isLoading.value\n                              ? SliverList(\n                                  delegate: SliverChildBuilderDelegate(\n                                      (context, index) {\n                                    return const VideoCardHSkeleton();\n                                  }, childCount: 10),\n                                )\n                              : const NoData(),\n                    );\n                  } else {\n                    return HttpError(\n                      errMsg: snapshot.data['msg'],\n                      fn: () {},\n                    );\n                  }\n                } else {\n                  return HttpError(\n                    errMsg: snapshot.data['msg'],\n                    fn: () {},\n                  );\n                }\n              } else {\n                return SliverList(\n                  delegate: SliverChildBuilderDelegate((context, index) {\n                    return const VideoCardHSkeleton();\n                  }, childCount: 10),\n                );\n              }\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_article/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/member/article.dart';\n\nclass MemberArticleController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  late int mid;\n  int pn = 1;\n  String? offset;\n  bool hasMore = true;\n  String? wWebid;\n  RxBool isLoading = false.obs;\n  RxList<MemberArticleItemModel> articleList = <MemberArticleItemModel>[].obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    mid = int.parse(Get.parameters['mid']!);\n  }\n\n  // 获取wWebid\n  Future getWWebid() async {\n    var res = await MemberHttp.getWWebid(mid: mid);\n    if (res['status']) {\n      wWebid = res['data'];\n    } else {\n      wWebid = '-1';\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  Future getMemberArticle(type) async {\n    if (isLoading.value) {\n      return;\n    }\n    isLoading.value = true;\n    if (wWebid == null) {\n      await getWWebid();\n    }\n    if (type == 'init') {\n      pn = 1;\n      articleList.clear();\n    }\n    var res = await MemberHttp.getMemberArticle(\n      mid: mid,\n      pn: pn,\n      offset: offset,\n      wWebid: wWebid!,\n    );\n    if (res['status']) {\n      offset = res['data'].offset;\n      hasMore = res['data'].hasMore!;\n      if (type == 'init') {\n        articleList.value = res['data'].items;\n      }\n      if (type == 'onLoad') {\n        articleList.addAll(res['data'].items);\n      }\n      pn += 1;\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n    isLoading.value = false;\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_article/index.dart",
    "content": "library member_article;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/member_article/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/skeleton.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'controller.dart';\n\nclass MemberArticlePage extends StatefulWidget {\n  const MemberArticlePage({super.key});\n\n  @override\n  State<MemberArticlePage> createState() => _MemberArticlePageState();\n}\n\nclass _MemberArticlePageState extends State<MemberArticlePage> {\n  late MemberArticleController _memberArticleController;\n  late Future _futureBuilderFuture;\n  late ScrollController scrollController;\n  late int mid;\n\n  @override\n  void initState() {\n    super.initState();\n    mid = int.parse(Get.parameters['mid']!);\n    final String heroTag = Utils.makeHeroTag(mid);\n    _memberArticleController = Get.put(MemberArticleController(), tag: heroTag);\n    _futureBuilderFuture = _memberArticleController.getMemberArticle('init');\n    scrollController = _memberArticleController.scrollController;\n\n    scrollController.addListener(_scrollListener);\n  }\n\n  void _scrollListener() {\n    if (scrollController.position.pixels >=\n        scrollController.position.maxScrollExtent - 200) {\n      EasyThrottle.throttle(\n          'member_archives', const Duration(milliseconds: 500), () {\n        _memberArticleController.getMemberArticle('onLoad');\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        centerTitle: false,\n        title: const Text('Ta的图文', style: TextStyle(fontSize: 16)),\n      ),\n      body: FutureBuilder(\n        future: _futureBuilderFuture,\n        builder: (BuildContext context, snapshot) {\n          if (snapshot.connectionState == ConnectionState.done) {\n            if (snapshot.data != null) {\n              return _buildContent(snapshot.data as Map);\n            } else {\n              return _buildError(snapshot.data['msg']);\n            }\n          } else {\n            return ListView.builder(\n              itemCount: 10,\n              itemBuilder: (BuildContext context, int index) {\n                return _buildSkeleton();\n              },\n            );\n          }\n        },\n      ),\n    );\n  }\n\n  Widget _buildContent(Map data) {\n    RxList list = _memberArticleController.articleList;\n    if (data['status']) {\n      return Obx(\n        () => list.isNotEmpty\n            ? ListView.separated(\n                controller: scrollController,\n                itemCount: list.length,\n                separatorBuilder: (BuildContext context, int index) {\n                  return Divider(\n                    height: 10,\n                    color: Theme.of(context).dividerColor.withOpacity(0.15),\n                  );\n                },\n                itemBuilder: (BuildContext context, int index) {\n                  return _buildListItem(list[index]);\n                },\n              )\n            : const CustomScrollView(\n                physics: NeverScrollableScrollPhysics(),\n                slivers: [\n                  NoData(),\n                ],\n              ),\n      );\n    } else {\n      return _buildError(data['msg']);\n    }\n  }\n\n  Widget _buildListItem(dynamic item) {\n    return ListTile(\n      onTap: () {\n        Get.toNamed('/opus', parameters: {\n          'title': item.content,\n          'id': item.opusId,\n          'articleType': 'opus',\n        });\n      },\n      leading: NetworkImgLayer(\n        width: 50,\n        height: 50,\n        type: 'emote',\n        src: item.cover['url'],\n      ),\n      title: Text(\n        item.content,\n        maxLines: 2,\n        overflow: TextOverflow.ellipsis,\n      ),\n      subtitle: Padding(\n        padding: const EdgeInsets.only(top: 4),\n        child: Text(\n          '${item.stat[\"like\"]}人点赞',\n          style: TextStyle(\n            fontSize: 12,\n            color: Theme.of(context).colorScheme.outline,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildError(String errMsg) {\n    return CustomScrollView(\n      physics: const NeverScrollableScrollPhysics(),\n      slivers: [\n        SliverToBoxAdapter(\n          child: HttpError(\n            errMsg: errMsg,\n            fn: () {},\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildSkeleton() {\n    return Skeleton(\n      child: ListTile(\n        leading: Container(\n          width: 50,\n          height: 50,\n          decoration: BoxDecoration(\n            color: Theme.of(context).colorScheme.onInverseSurface,\n            borderRadius: BorderRadius.circular(4),\n          ),\n        ),\n        title: Container(\n          height: 16,\n          color: Theme.of(context).colorScheme.onInverseSurface,\n        ),\n        subtitle: Container(\n          height: 11,\n          color: Theme.of(context).colorScheme.onInverseSurface,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_coin/controller.dart",
    "content": "import 'package:get/get.dart';\n\nclass MemberCoinController extends GetxController {}\n"
  },
  {
    "path": "lib/pages/member_coin/index.dart",
    "content": "library member_coin;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/member_coin/view.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass MemberCoinPage extends StatefulWidget {\n  const MemberCoinPage({super.key});\n\n  @override\n  State<MemberCoinPage> createState() => _MemberCoinPageState();\n}\n\nclass _MemberCoinPageState extends State<MemberCoinPage> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold();\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_coin/widgets/item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/member/coin.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass MemberCoinsItem extends StatelessWidget {\n  final MemberCoinsDataModel coinItem;\n\n  const MemberCoinsItem({\n    Key? key,\n    required this.coinItem,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(coinItem.aid);\n    return Card(\n      elevation: 0,\n      clipBehavior: Clip.hardEdge,\n      margin: EdgeInsets.zero,\n      child: InkWell(\n        onTap: () async {\n          int cid =\n              await SearchHttp.ab2c(aid: coinItem.aid, bvid: coinItem.bvid);\n          Get.toNamed('/video?bvid=${coinItem.bvid}&cid=$cid',\n              arguments: {'videoItem': coinItem, 'heroTag': heroTag});\n        },\n        child: Column(\n          children: [\n            AspectRatio(\n              aspectRatio: StyleString.aspectRatio,\n              child: LayoutBuilder(builder: (context, boxConstraints) {\n                double maxWidth = boxConstraints.maxWidth;\n                double maxHeight = boxConstraints.maxHeight;\n                return Stack(\n                  children: [\n                    NetworkImgLayer(\n                      src: coinItem.pic,\n                      width: maxWidth,\n                      height: maxHeight,\n                    ),\n                    if (coinItem.duration != null)\n                      PBadge(\n                        bottom: 6,\n                        right: 6,\n                        type: 'gray',\n                        text: Utils.timeFormat(coinItem.duration),\n                      )\n                  ],\n                );\n              }),\n            ),\n            Padding(\n              padding: const EdgeInsets.fromLTRB(5, 6, 0, 0),\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Text(\n                    coinItem.title!,\n                    maxLines: 2,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                  const SizedBox(height: 4),\n                  Row(\n                    children: [\n                      StatView(view: coinItem.view),\n                      const Spacer(),\n                      Text(\n                        Utils.CustomStamp_str(\n                            timestamp: coinItem.pubdate, date: 'MM-DD'),\n                        style: TextStyle(\n                          fontSize: 11,\n                          color: Theme.of(context).colorScheme.outline,\n                        ),\n                      ),\n                      const SizedBox(width: 6)\n                    ],\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_dynamics/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/dynamics/result.dart';\n\nclass MemberDynamicsController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  late int mid;\n  String offset = '';\n  int count = 0;\n  bool hasMore = true;\n  RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    mid = int.parse(Get.parameters['mid']!);\n  }\n\n  Future getMemberDynamic(type) async {\n    if (type == 'onRefresh') {\n      offset = '';\n      dynamicsList.clear();\n    }\n    if (offset == '-1') {\n      return;\n    }\n    var res = await MemberHttp.memberDynamic(\n      offset: offset,\n      mid: mid,\n    );\n    if (res['status']) {\n      dynamicsList.addAll(res['data'].items);\n      offset = res['data'].offset != '' ? res['data'].offset : '-1';\n      hasMore = res['data'].hasMore;\n    }\n    return res;\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    getMemberDynamic('onLoad');\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_dynamics/index.dart",
    "content": "library member_dynamics;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/member_dynamics/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/pages/member_dynamics/index.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport '../../common/widgets/http_error.dart';\nimport '../../models/dynamics/result.dart';\nimport '../dynamics/widgets/dynamic_panel.dart';\n\nclass MemberDynamicsPage extends StatefulWidget {\n  const MemberDynamicsPage({super.key});\n\n  @override\n  State<MemberDynamicsPage> createState() => _MemberDynamicsPageState();\n}\n\nclass _MemberDynamicsPageState extends State<MemberDynamicsPage> {\n  late MemberDynamicsController _memberDynamicController;\n  late Future _futureBuilderFuture;\n  late ScrollController scrollController;\n  late int mid;\n\n  @override\n  void initState() {\n    super.initState();\n    mid = int.parse(Get.parameters['mid']!);\n    final String heroTag = Utils.makeHeroTag(mid);\n    _memberDynamicController =\n        Get.put(MemberDynamicsController(), tag: heroTag);\n    _futureBuilderFuture =\n        _memberDynamicController.getMemberDynamic('onRefresh');\n    scrollController = _memberDynamicController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle(\n              'member_dynamics', const Duration(milliseconds: 1000), () {\n            _memberDynamicController.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _memberDynamicController.scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        centerTitle: false,\n        title: Text('他的动态', style: Theme.of(context).textTheme.titleMedium),\n      ),\n      body: CustomScrollView(\n        controller: _memberDynamicController.scrollController,\n        slivers: [\n          FutureBuilder(\n            future: _futureBuilderFuture,\n            builder: (context, snapshot) {\n              if (snapshot.connectionState == ConnectionState.done) {\n                if (snapshot.data != null) {\n                  Map data = snapshot.data as Map;\n                  RxList<DynamicItemModel> list =\n                      _memberDynamicController.dynamicsList;\n                  if (data['status']) {\n                    return Obx(\n                      () => list.isNotEmpty\n                          ? SliverList(\n                              delegate: SliverChildBuilderDelegate(\n                                (context, index) {\n                                  return DynamicPanel(item: list[index]);\n                                },\n                                childCount: list.length,\n                              ),\n                            )\n                          : const SliverToBoxAdapter(),\n                    );\n                  } else {\n                    return HttpError(\n                      errMsg: snapshot.data['msg'],\n                      fn: () {},\n                    );\n                  }\n                } else {\n                  return HttpError(\n                    errMsg: snapshot.data['msg'],\n                    fn: () {},\n                  );\n                }\n              } else {\n                return const SliverToBoxAdapter();\n              }\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_like/controller.dart",
    "content": "import 'package:get/get.dart';\n\nclass MemberLikeController extends GetxController {}\n"
  },
  {
    "path": "lib/pages/member_like/index.dart",
    "content": "library member_like;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/member_like/view.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass MemberLikePage extends StatefulWidget {\n  const MemberLikePage({super.key});\n\n  @override\n  State<MemberLikePage> createState() => _MemberLikePageState();\n}\n\nclass _MemberLikePageState extends State<MemberLikePage> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold();\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_like/widgets/item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/member/like.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass MemberLikeItem extends StatelessWidget {\n  final MemberLikeDataModel likeItem;\n\n  const MemberLikeItem({\n    Key? key,\n    required this.likeItem,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(likeItem.aid);\n    return Card(\n      elevation: 0,\n      clipBehavior: Clip.hardEdge,\n      margin: EdgeInsets.zero,\n      child: InkWell(\n        onTap: () async {\n          int cid =\n              await SearchHttp.ab2c(aid: likeItem.aid, bvid: likeItem.bvid);\n          Get.toNamed('/video?bvid=${likeItem.bvid}&cid=$cid',\n              arguments: {'videoItem': likeItem, 'heroTag': heroTag});\n        },\n        child: Column(\n          children: [\n            AspectRatio(\n              aspectRatio: StyleString.aspectRatio,\n              child: LayoutBuilder(builder: (context, boxConstraints) {\n                double maxWidth = boxConstraints.maxWidth;\n                double maxHeight = boxConstraints.maxHeight;\n                return Stack(\n                  children: [\n                    NetworkImgLayer(\n                      src: likeItem.pic,\n                      width: maxWidth,\n                      height: maxHeight,\n                    ),\n                    if (likeItem.duration != null)\n                      PBadge(\n                        bottom: 6,\n                        right: 6,\n                        type: 'gray',\n                        text: Utils.timeFormat(likeItem.duration),\n                      )\n                  ],\n                );\n              }),\n            ),\n            Padding(\n              padding: const EdgeInsets.fromLTRB(5, 6, 0, 0),\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Text(\n                    likeItem.title!,\n                    maxLines: 2,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                  const SizedBox(height: 4),\n                  Row(\n                    children: [\n                      StatView(view: likeItem.stat!.view),\n                      const Spacer(),\n                      Text(\n                        Utils.CustomStamp_str(\n                            timestamp: likeItem.pubdate, date: 'MM-DD'),\n                        style: TextStyle(\n                          fontSize: 11,\n                          color: Theme.of(context).colorScheme.outline,\n                        ),\n                      ),\n                      const SizedBox(width: 6)\n                    ],\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_search/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/member/archive.dart';\n\nclass MemberSearchController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  Rx<TextEditingController> controller = TextEditingController().obs;\n  final FocusNode searchFocusNode = FocusNode();\n  RxString searchKeyWord = ''.obs;\n  String hintText = '搜索';\n  RxString loadingStatus = 'init'.obs;\n  RxString loadingText = '加载中...'.obs;\n  bool hasRequest = false;\n  late int mid;\n  RxString uname = ''.obs;\n  int archivePn = 1;\n  int archiveCount = 0;\n  RxList<VListItemModel> archiveList = <VListItemModel>[].obs;\n  int dynamic_pn = 1;\n  RxList<VListItemModel> dynamicList = <VListItemModel>[].obs;\n\n  int ps = 30;\n\n  @override\n  void onInit() {\n    super.onInit();\n    mid = int.parse(Get.parameters['mid']!);\n    uname.value = Get.parameters['uname']!;\n  }\n\n  // 清空搜索\n  void onClear() {\n    if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {\n      controller.value.clear();\n      searchKeyWord.value = '';\n    } else {\n      Get.back();\n    }\n  }\n\n  void onChange(value) {\n    searchKeyWord.value = value;\n  }\n\n  //  提交搜索内容\n  void submit() {\n    loadingStatus.value = 'loading';\n    if (hasRequest) {\n      archivePn = 1;\n      searchArchives();\n    }\n  }\n\n  // 搜索视频\n  Future searchArchives({type = 'init'}) async {\n    if (type == 'onLoad' && loadingText.value == '没有更多了') {\n      return;\n    }\n    var res = await MemberHttp.memberArchive(\n      mid: mid,\n      pn: archivePn,\n      keyword: controller.value.text,\n      order: 'pubdate',\n    );\n    if (res['status']) {\n      if (type == 'init' || archivePn == 1) {\n        archiveList.value = res['data'].list.vlist;\n      } else {\n        archiveList.addAll(res['data'].list.vlist);\n      }\n      archiveCount = res['data'].page['count'];\n      if (archiveList.length == archiveCount) {\n        loadingText.value = '没有更多了';\n      }\n      archivePn += 1;\n      hasRequest = true;\n    }\n    // loadingStatus.value = 'finish';\n    return res;\n  }\n\n  // 搜索动态\n  Future searchDynamic() async {}\n\n  //\n  onLoad() {\n    searchArchives(type: 'onLoad');\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_search/index.dart",
    "content": "library member_search;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/member_search/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:pilipala/common/widgets/video_card_h.dart';\n\nimport 'controller.dart';\n\nclass MemberSearchPage extends StatefulWidget {\n  const MemberSearchPage({super.key});\n\n  @override\n  State<MemberSearchPage> createState() => _MemberSearchPageState();\n}\n\nclass _MemberSearchPageState extends State<MemberSearchPage>\n    with SingleTickerProviderStateMixin {\n  final MemberSearchController _memberSearchCtr =\n      Get.put(MemberSearchController());\n  late ScrollController scrollController;\n\n  @override\n  void initState() {\n    super.initState();\n    scrollController = _memberSearchCtr.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('history', const Duration(seconds: 1), () {\n            _memberSearchCtr.onLoad();\n          });\n        }\n      },\n    );\n    // _tabController = TabController(length: 2, vsync: this);\n  }\n\n  @override\n  void dispose() {\n    // _tabController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        actions: [\n          IconButton(\n              onPressed: () => _memberSearchCtr.submit(),\n              icon: const Icon(CupertinoIcons.search, size: 22)),\n          const SizedBox(width: 10)\n        ],\n        title: Obx(\n          () => TextField(\n            autofocus: true,\n            focusNode: _memberSearchCtr.searchFocusNode,\n            controller: _memberSearchCtr.controller.value,\n            textInputAction: TextInputAction.search,\n            onChanged: (value) => _memberSearchCtr.onChange(value),\n            decoration: InputDecoration(\n              hintText: _memberSearchCtr.hintText,\n              border: InputBorder.none,\n              suffixIcon: IconButton(\n                icon: Icon(\n                  Icons.clear,\n                  size: 22,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n                onPressed: () => _memberSearchCtr.onClear(),\n              ),\n            ),\n            onSubmitted: (String value) => _memberSearchCtr.submit(),\n          ),\n        ),\n      ),\n      body: Obx(\n        () => Column(\n          children: _memberSearchCtr.loadingStatus.value == 'init'\n              ? [\n                  Expanded(\n                    child: Center(\n                      child: Text('搜索「${_memberSearchCtr.uname.value}」的动态、视频'),\n                    ),\n                  ),\n                ]\n              : [\n                  // TabBar(\n                  //   controller: _tabController,\n                  //   tabs: const [\n                  //     Tab(text: \"视频\"),\n                  //     Tab(text: \"动态\"),\n                  //   ],\n                  // ),\n                  Expanded(\n                    child:\n                        // TabBarView(\n                        //   controller: _tabController,\n                        //   children: [\n                        FutureBuilder(\n                      future: _memberSearchCtr.searchArchives(),\n                      builder: (context, snapshot) {\n                        if (snapshot.connectionState == ConnectionState.done) {\n                          Map data = snapshot.data as Map;\n                          if (data['status']) {\n                            return Obx(\n                              () => _memberSearchCtr.archiveList.isNotEmpty\n                                  ? ListView.builder(\n                                      controller: scrollController,\n                                      itemCount:\n                                          _memberSearchCtr.archiveList.length +\n                                              1,\n                                      itemBuilder: (context, index) {\n                                        if (index ==\n                                            _memberSearchCtr\n                                                .archiveList.length) {\n                                          return Container(\n                                            height: MediaQuery.of(context)\n                                                    .padding\n                                                    .bottom +\n                                                60,\n                                            padding: EdgeInsets.only(\n                                                bottom: MediaQuery.of(context)\n                                                    .padding\n                                                    .bottom),\n                                            child: Center(\n                                              child: Obx(\n                                                () => Text(\n                                                  _memberSearchCtr\n                                                      .loadingText.value,\n                                                  style: TextStyle(\n                                                      color: Theme.of(context)\n                                                          .colorScheme\n                                                          .outline,\n                                                      fontSize: 13),\n                                                ),\n                                              ),\n                                            ),\n                                          );\n                                        } else {\n                                          return VideoCardH(\n                                              videoItem: _memberSearchCtr\n                                                  .archiveList[index]);\n                                        }\n                                      },\n                                    )\n                                  : _memberSearchCtr.loadingStatus.value ==\n                                          'loading'\n                                      ? ListView.builder(\n                                          itemCount: 10,\n                                          itemBuilder: (context, index) {\n                                            return const VideoCardHSkeleton();\n                                          },\n                                        )\n                                      : const CustomScrollView(\n                                          slivers: <Widget>[\n                                            NoData(),\n                                          ],\n                                        ),\n                            );\n                          } else {\n                            return CustomScrollView(\n                              slivers: <Widget>[\n                                HttpError(\n                                  errMsg: data['msg'],\n                                  fn: () => setState(() {}),\n                                )\n                              ],\n                            );\n                          }\n                        } else {\n                          // 骨架屏\n                          return ListView.builder(\n                            itemCount: 10,\n                            itemBuilder: (context, index) {\n                              return const VideoCardHSkeleton();\n                            },\n                          );\n                        }\n                      },\n                    ),\n                    //   ],\n                    // ),\n                  ),\n                ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_seasons/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/member/seasons.dart';\n\nclass MemberSeasonsController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  late int mid;\n  int? seasonId;\n  int? seriesId;\n  late String category;\n  int pn = 1;\n  int ps = 30;\n  int count = 0;\n  RxList<MemberArchiveItem> seasonsList = <MemberArchiveItem>[].obs;\n  late Map page;\n\n  @override\n  void onInit() {\n    super.onInit();\n    mid = int.parse(Get.parameters['mid']!);\n    category = Get.parameters['category']!;\n    if (category == '0') {\n      seasonId = int.parse(Get.parameters['seasonId']!);\n    }\n    if (category == '1') {\n      seriesId = int.parse(Get.parameters['seriesId']!);\n    }\n  }\n\n  // 获取专栏详情 0: 专栏 1: 系列\n  Future getSeasonDetail(type) async {\n    if (type == 'onRefresh') {\n      pn = 1;\n    }\n    var res = await MemberHttp.getSeasonDetail(\n      mid: mid,\n      seasonId: seasonId!,\n      pn: pn,\n      ps: ps,\n      sortReverse: false,\n    );\n    if (res['status']) {\n      seasonsList.addAll(res['data'].archives);\n      page = res['data'].page;\n      pn += 1;\n    }\n    return res;\n  }\n\n  // 获取系列详情 0: 专栏 1: 系列\n  Future getSeriesDetail(type) async {\n    if (type == 'onRefresh') {\n      pn = 1;\n    }\n    var res = await MemberHttp.getSeriesDetail(\n      mid: mid,\n      seriesId: seriesId!,\n      pn: pn,\n      currentMid: 17340771,\n    );\n    if (res['status']) {\n      seasonsList.addAll(res['data'].seriesList);\n      page = res['data'].page;\n      pn += 1;\n    }\n    return res;\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    if (category == '0') {\n      getSeasonDetail('onLoad');\n    }\n    if (category == '1') {\n      getSeriesDetail('onLoad');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_seasons/index.dart",
    "content": "library member_seasons;\n\nexport 'controller.dart';\nexport 'view.dart';\n"
  },
  {
    "path": "lib/pages/member_seasons/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'controller.dart';\nimport 'widgets/item.dart';\n\nclass MemberSeasonsPage extends StatefulWidget {\n  const MemberSeasonsPage({super.key});\n\n  @override\n  State<MemberSeasonsPage> createState() => _MemberSeasonsPageState();\n}\n\nclass _MemberSeasonsPageState extends State<MemberSeasonsPage> {\n  final MemberSeasonsController _memberSeasonsController =\n      Get.put(MemberSeasonsController());\n  late Future _futureBuilderFuture;\n  late ScrollController scrollController;\n  late String category;\n\n  @override\n  void initState() {\n    super.initState();\n    category = Get.parameters['category']!;\n    _futureBuilderFuture = category == '0'\n        ? _memberSeasonsController.getSeasonDetail('onRefresh')\n        : _memberSeasonsController.getSeriesDetail('onRefresh');\n    scrollController = _memberSeasonsController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle(\n              'member_archives', const Duration(milliseconds: 500), () {\n            _memberSeasonsController.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        centerTitle: false,\n        title: Text(Get.parameters['seasonName']!,\n            style: Theme.of(context).textTheme.titleMedium),\n      ),\n      body: Padding(\n        padding: const EdgeInsets.only(\n          left: StyleString.safeSpace,\n          right: StyleString.safeSpace,\n        ),\n        child: SingleChildScrollView(\n          controller: _memberSeasonsController.scrollController,\n          child: FutureBuilder(\n            future: _futureBuilderFuture,\n            builder: (context, snapshot) {\n              if (snapshot.connectionState == ConnectionState.done) {\n                if (snapshot.data != null) {\n                  Map data = snapshot.data as Map;\n                  List list = _memberSeasonsController.seasonsList;\n                  if (data['status']) {\n                    return Obx(\n                      () => list.isNotEmpty\n                          ? LayoutBuilder(\n                              builder: (context, boxConstraints) {\n                                return GridView.builder(\n                                  gridDelegate:\n                                      const SliverGridDelegateWithFixedCrossAxisCount(\n                                    crossAxisCount: 2,\n                                    crossAxisSpacing: StyleString.safeSpace,\n                                    mainAxisSpacing: StyleString.safeSpace,\n                                    childAspectRatio: 0.94,\n                                  ),\n                                  physics: const NeverScrollableScrollPhysics(),\n                                  shrinkWrap: true,\n                                  itemCount: _memberSeasonsController\n                                      .seasonsList.length,\n                                  itemBuilder: (context, i) {\n                                    return MemberSeasonsItem(\n                                      seasonItem: _memberSeasonsController\n                                          .seasonsList[i],\n                                    );\n                                  },\n                                );\n                              },\n                            )\n                          : const SizedBox(),\n                    );\n                  } else {\n                    return const SizedBox();\n                  }\n                } else {\n                  return const SizedBox();\n                }\n              } else {\n                return const SizedBox();\n              }\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/member_seasons/widgets/item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/utils/image_save.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass MemberSeasonsItem extends StatelessWidget {\n  final dynamic seasonItem;\n\n  const MemberSeasonsItem({\n    Key? key,\n    required this.seasonItem,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(seasonItem.aid);\n    return Card(\n      elevation: 0,\n      clipBehavior: Clip.hardEdge,\n      margin: EdgeInsets.zero,\n      child: InkWell(\n        onTap: () async {\n          int cid =\n              await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid);\n          Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid',\n              arguments: {'videoItem': seasonItem, 'heroTag': heroTag});\n        },\n        onLongPress: () => imageSaveDialog(\n          context,\n          seasonItem,\n          SmartDialog.dismiss,\n        ),\n        child: Column(\n          children: [\n            AspectRatio(\n              aspectRatio: StyleString.aspectRatio,\n              child: LayoutBuilder(builder: (context, boxConstraints) {\n                double maxWidth = boxConstraints.maxWidth;\n                double maxHeight = boxConstraints.maxHeight;\n                return Stack(\n                  children: [\n                    Hero(\n                      tag: heroTag,\n                      child: NetworkImgLayer(\n                        src: seasonItem.pic,\n                        width: maxWidth,\n                        height: maxHeight,\n                      ),\n                    ),\n                    if (seasonItem.pubdate != null)\n                      PBadge(\n                        bottom: 6,\n                        right: 6,\n                        type: 'gray',\n                        text: Utils.timeFormat(seasonItem.duration),\n                      )\n                  ],\n                );\n              }),\n            ),\n            Padding(\n              padding: const EdgeInsets.fromLTRB(5, 6, 0, 0),\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Text(\n                    seasonItem.title,\n                    maxLines: 2,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                  const SizedBox(height: 4),\n                  Row(\n                    children: [\n                      StatView(view: seasonItem.view),\n                      const Spacer(),\n                      Text(\n                        Utils.CustomStamp_str(\n                            timestamp: seasonItem.pubdate, date: 'YY-MM-DD'),\n                        style: TextStyle(\n                          fontSize: 11,\n                          color: Theme.of(context).colorScheme.outline,\n                        ),\n                      ),\n                      const SizedBox(width: 6)\n                    ],\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/message/at/controller.dart",
    "content": "import 'package:get/get.dart';\n\nclass MessageAtController extends GetxController {}\n"
  },
  {
    "path": "lib/pages/message/at/index.dart",
    "content": "library message_at;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/message/at/view.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass MessageAtPage extends StatefulWidget {\n  const MessageAtPage({super.key});\n\n  @override\n  State<MessageAtPage> createState() => _MessageAtPageState();\n}\n\nclass _MessageAtPageState extends State<MessageAtPage> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('@我的'),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/message/like/controller.dart",
    "content": "import 'package:get/get.dart';\nimport 'package:pilipala/http/msg.dart';\nimport 'package:pilipala/models/msg/like.dart';\n\nclass MessageLikeController extends GetxController {\n  Cursor? cursor;\n  RxList<MessageLikeItem> likeItems = <MessageLikeItem>[].obs;\n\n  Future queryMessageLike({String type = 'init'}) async {\n    if (cursor != null && cursor!.isEnd == true) {\n      return {};\n    }\n    var params = {\n      if (type == 'onLoad') 'id': cursor!.id,\n      if (type == 'onLoad') 'likeTime': cursor!.time,\n    };\n    var res = await MsgHttp.messageLike(\n        id: params['id'], likeTime: params['likeTime']);\n    if (res['status']) {\n      cursor = res['data'].total.cursor;\n      likeItems.addAll(res['data'].total.items);\n    }\n    return res;\n  }\n\n  Future expandedUsersAvatar(i) async {\n    likeItems[i].isExpand = !likeItems[i].isExpand;\n    likeItems.refresh();\n  }\n}\n"
  },
  {
    "path": "lib/pages/message/like/index.dart",
    "content": "library message_like;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/message/like/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/msg/like.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'controller.dart';\n\nclass MessageLikePage extends StatefulWidget {\n  const MessageLikePage({super.key});\n\n  @override\n  State<MessageLikePage> createState() => _MessageLikePageState();\n}\n\nclass _MessageLikePageState extends State<MessageLikePage> {\n  final MessageLikeController _messageLikeCtr =\n      Get.put(MessageLikeController());\n  late Future _futureBuilderFuture;\n  final ScrollController scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _messageLikeCtr.queryMessageLike();\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('follow', const Duration(seconds: 1), () {\n            _messageLikeCtr.queryMessageLike(type: 'onLoad');\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('收到的赞'),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async {\n          await _messageLikeCtr.queryMessageLike(type: 'init');\n        },\n        child: FutureBuilder(\n          future: _futureBuilderFuture,\n          builder: (BuildContext context, AsyncSnapshot snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              if (snapshot.data == null) {\n                return const SizedBox();\n              }\n              if (snapshot.data['status']) {\n                final likeItems = _messageLikeCtr.likeItems;\n                return Obx(\n                  () => ListView.separated(\n                    controller: scrollController,\n                    itemBuilder: (context, index) => LikeItem(\n                      item: likeItems[index],\n                      index: index,\n                      messageLikeCtr: _messageLikeCtr,\n                    ),\n                    itemCount: likeItems.length,\n                    separatorBuilder: (BuildContext context, int index) {\n                      return Divider(\n                        indent: 66,\n                        endIndent: 14,\n                        height: 1,\n                        color: Colors.grey.withOpacity(0.1),\n                      );\n                    },\n                  ),\n                );\n              } else {\n                // 请求错误\n                return CustomScrollView(\n                  slivers: [\n                    HttpError(\n                      errMsg: snapshot.data['msg'],\n                      fn: () {\n                        setState(() {\n                          _futureBuilderFuture =\n                              _messageLikeCtr.queryMessageLike();\n                        });\n                      },\n                    )\n                  ],\n                );\n              }\n            } else {\n              return const SizedBox();\n            }\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass LikeItem extends StatelessWidget {\n  final MessageLikeItem item;\n  final int index;\n  final MessageLikeController messageLikeCtr;\n\n  const LikeItem(\n      {super.key,\n      required this.item,\n      required this.index,\n      required this.messageLikeCtr});\n\n  @override\n  Widget build(BuildContext context) {\n    Color outline = Theme.of(context).colorScheme.outline;\n    final nickNameList = item.users!.map((e) => e.nickname).take(2).toList();\n    int usersLen = item.users!.length > 3 ? 3 : item.users!.length;\n    final String bvid = item.item!.uri!.split('/').last;\n    // 页码\n    final String page =\n        item.item!.nativeUri!.split('page=').last.split('&').first;\n    // 根评论id\n    final String commentRootId =\n        item.item!.nativeUri!.split('comment_root_id=').last.split('&').first;\n    // 二级评论id\n    final String commentSecondaryId =\n        item.item!.nativeUri!.split('comment_secondary_id=').last;\n\n    return InkWell(\n      onTap: () async {\n        try {\n          final int cid = await SearchHttp.ab2c(bvid: bvid);\n          final String heroTag = Utils.makeHeroTag(bvid);\n          Get.toNamed<dynamic>(\n            '/video?bvid=$bvid&cid=$cid',\n            arguments: <String, String?>{\n              'pic': '',\n              'heroTag': heroTag,\n            },\n          );\n        } catch (_) {\n          SmartDialog.showToast('视频可能失效了');\n        }\n      },\n      child: Stack(\n        children: [\n          Padding(\n            padding: const EdgeInsets.all(14),\n            child: Row(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                GestureDetector(\n                  behavior: HitTestBehavior.translucent,\n                  onTap: () {\n                    if (usersLen == 1) {\n                      final String heroTag =\n                          Utils.makeHeroTag(item.users!.first.mid);\n                      Get.toNamed('/member?mid=${item.users!.first.mid}',\n                          arguments: {\n                            'face': item.users!.first.avatar,\n                            'heroTag': heroTag\n                          });\n                    } else {\n                      messageLikeCtr.expandedUsersAvatar(index);\n                    }\n                  },\n                  // 多个头像层叠\n                  child: SizedBox(\n                    width: 50,\n                    height: 50,\n                    child: Stack(\n                      children: [\n                        for (var i = 0; i < usersLen; i++)\n                          Positioned(\n                            top: i % 2 * (50 / (usersLen >= 2 ? 2 : 1)),\n                            left: i / 2 * (50 / (usersLen >= 2 ? 2 : 1)),\n                            child: NetworkImgLayer(\n                              width: 50 / (usersLen >= 2 ? 2 : 1),\n                              height: 50 / (usersLen >= 2 ? 2 : 1),\n                              type: 'avatar',\n                              src: item.users![i].avatar,\n                            ),\n                          ),\n                      ],\n                    ),\n                  ),\n                ),\n                const SizedBox(width: 10),\n                Expanded(\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Text.rich(TextSpan(children: [\n                        TextSpan(text: nickNameList.join('、')),\n                        const TextSpan(text: ' '),\n                        if (item.counts! > 1)\n                          TextSpan(\n                            text: '等总计${item.counts}人',\n                            style: TextStyle(color: outline),\n                          ),\n                        TextSpan(\n                          text: '赞了我的${item.item!.business}',\n                          style: TextStyle(color: outline),\n                        ),\n                      ])),\n                      const SizedBox(height: 4),\n                      Text(\n                        Utils.dateFormat(item.likeTime!, formatType: 'detail'),\n                        style: TextStyle(color: outline),\n                      ),\n                    ],\n                  ),\n                ),\n                const SizedBox(width: 25),\n                if (item.item!.type! == 'reply')\n                  Container(\n                    width: 60,\n                    height: 60,\n                    padding: const EdgeInsets.all(4),\n                    child: Text(\n                      item.item!.title!,\n                      maxLines: 4,\n                      style: const TextStyle(fontSize: 12, letterSpacing: 0.3),\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ),\n                if (item.item!.type! == 'video')\n                  NetworkImgLayer(\n                    width: 60,\n                    height: 60,\n                    src: item.item!.image,\n                  ),\n              ],\n            ),\n          ),\n          Positioned(\n            top: 0,\n            right: 0,\n            bottom: 0,\n            child: AnimatedContainer(\n              duration: const Duration(milliseconds: 300),\n              width: item.isExpand ? Get.size.width - 74 : 0,\n              color: Theme.of(context).colorScheme.secondaryContainer,\n              child: ListView.builder(\n                itemCount: item.users!.length,\n                scrollDirection: Axis.horizontal,\n                itemBuilder: (BuildContext context, int i) {\n                  return Padding(\n                    padding: EdgeInsets.fromLTRB(i == 0 ? 12 : 4, 8, 4, 0),\n                    child: Column(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        GestureDetector(\n                          onTap: () {\n                            final String heroTag =\n                                Utils.makeHeroTag(item.users![i].mid);\n                            Get.toNamed(\n                              '/member?mid=${item.users![i].mid}',\n                              arguments: {\n                                'face': item.users![i].avatar,\n                                'heroTag': heroTag\n                              },\n                            );\n                          },\n                          child: NetworkImgLayer(\n                            width: 42,\n                            height: 42,\n                            type: 'avatar',\n                            src: item.users![i].avatar,\n                          ),\n                        ),\n                        const SizedBox(height: 6),\n                        SizedBox(\n                          width: 68,\n                          child: Text(\n                            textAlign: TextAlign.center,\n                            item.users![i].nickname!,\n                            maxLines: 1,\n                            overflow: TextOverflow.clip,\n                            style: TextStyle(color: outline),\n                          ),\n                        ),\n                      ],\n                    ),\n                  );\n                },\n              ),\n            ),\n          ),\n          Positioned(\n            top: 0,\n            left: 0,\n            bottom: 0,\n            child: GestureDetector(\n              onTap: () {\n                messageLikeCtr.expandedUsersAvatar(index);\n              },\n              child: AnimatedContainer(\n                duration: const Duration(milliseconds: 300),\n                width: item.isExpand ? 74 : 0,\n                color: Colors.black.withOpacity(0.3),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/message/reply/controller.dart",
    "content": "import 'package:get/get.dart';\nimport 'package:pilipala/http/msg.dart';\nimport 'package:pilipala/models/msg/reply.dart';\n\nclass MessageReplyController extends GetxController {\n  Cursor? cursor;\n  RxList<MessageReplyItem> replyItems = <MessageReplyItem>[].obs;\n\n  Future queryMessageReply({String type = 'init'}) async {\n    if (cursor != null && cursor!.isEnd == true) {\n      return {};\n    }\n    var params = {\n      if (type == 'onLoad') 'id': cursor!.id,\n      if (type == 'onLoad') 'replyTime': cursor!.time,\n    };\n    var res = await MsgHttp.messageReply(\n        id: params['id'], replyTime: params['replyTime']);\n    if (res['status']) {\n      cursor = res['data'].cursor;\n      replyItems.addAll(res['data'].items);\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/message/reply/index.dart",
    "content": "library message_reply;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/message/reply/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/msg/reply.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'controller.dart';\n\nclass MessageReplyPage extends StatefulWidget {\n  const MessageReplyPage({super.key});\n\n  @override\n  State<MessageReplyPage> createState() => _MessageReplyPageState();\n}\n\nclass _MessageReplyPageState extends State<MessageReplyPage> {\n  final MessageReplyController _messageReplyCtr =\n      Get.put(MessageReplyController());\n  late Future _futureBuilderFuture;\n  final ScrollController scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _messageReplyCtr.queryMessageReply();\n    scrollController.addListener(\n      () async {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('follow', const Duration(seconds: 1), () {\n            _messageReplyCtr.queryMessageReply(type: 'onLoad');\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('回复我的'),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async {\n          await _messageReplyCtr.queryMessageReply(type: 'init');\n        },\n        child: FutureBuilder(\n          future: _futureBuilderFuture,\n          builder: (BuildContext context, AsyncSnapshot snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              if (snapshot.data == null) {\n                return const SizedBox();\n              }\n              if (snapshot.data['status']) {\n                final replyItems = _messageReplyCtr.replyItems;\n                return Obx(\n                  () => ListView.separated(\n                    controller: scrollController,\n                    itemBuilder: (context, index) =>\n                        ReplyItem(item: replyItems[index]),\n                    itemCount: replyItems.length,\n                    separatorBuilder: (BuildContext context, int index) {\n                      return Divider(\n                        indent: 66,\n                        endIndent: 14,\n                        height: 1,\n                        color: Colors.grey.withOpacity(0.1),\n                      );\n                    },\n                  ),\n                );\n              } else {\n                // 请求错误\n                return CustomScrollView(\n                  slivers: [\n                    HttpError(\n                      errMsg: snapshot.data['msg'],\n                      fn: () {\n                        setState(() {\n                          _futureBuilderFuture =\n                              _messageReplyCtr.queryMessageReply();\n                        });\n                      },\n                    )\n                  ],\n                );\n              }\n            } else {\n              return const SizedBox();\n            }\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass ReplyItem extends StatelessWidget {\n  final MessageReplyItem item;\n\n  const ReplyItem({super.key, required this.item});\n\n  @override\n  Widget build(BuildContext context) {\n    Color outline = Theme.of(context).colorScheme.outline;\n    final String heroTag = Utils.makeHeroTag(item.user!.mid);\n    final String bvid = item.item!.uri!.split('/').last;\n    // 页码\n    final String page =\n        item.item!.nativeUri!.split('page=').last.split('&').first;\n    // 根评论id\n    final String commentRootId =\n        item.item!.nativeUri!.split('comment_root_id=').last.split('&').first;\n    // 二级评论id\n    final String commentSecondaryId =\n        item.item!.nativeUri!.split('comment_secondary_id=').last;\n\n    return InkWell(\n      onTap: () async {\n        final int cid = await SearchHttp.ab2c(bvid: bvid);\n        final String heroTag = Utils.makeHeroTag(bvid);\n        Get.toNamed<dynamic>(\n          '/video?bvid=$bvid&cid=$cid',\n          arguments: <String, String?>{\n            'pic': '',\n            'heroTag': heroTag,\n          },\n        );\n      },\n      child: Padding(\n        padding: const EdgeInsets.all(14),\n        child: Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            GestureDetector(\n              onTap: () {\n                Get.toNamed('/member?mid=${item.user!.mid}',\n                    arguments: {'face': item.user!.avatar, 'heroTag': heroTag});\n              },\n              child: Hero(\n                tag: heroTag,\n                child: NetworkImgLayer(\n                  width: 42,\n                  height: 42,\n                  type: 'avatar',\n                  src: item.user!.avatar,\n                ),\n              ),\n            ),\n            const SizedBox(width: 10),\n            Expanded(\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Text.rich(TextSpan(children: [\n                    TextSpan(text: item.user!.nickname!),\n                    const TextSpan(text: ' '),\n                    if (item.item!.type! == 'video')\n                      TextSpan(\n                          text: '对我的视频发表了评论', style: TextStyle(color: outline)),\n                    if (item.item!.type! == 'reply')\n                      TextSpan(\n                        text: '回复了我的评论',\n                        style: TextStyle(color: outline),\n                      ),\n                  ])),\n                  const SizedBox(height: 6),\n                  Text.rich(\n                      maxLines: 2,\n                      overflow: TextOverflow.ellipsis,\n                      style: const TextStyle(letterSpacing: 0.3),\n                      buildContent(context, item.item)),\n                  if (item.item!.targetReplyContent != '') ...[\n                    const SizedBox(height: 2),\n                    Text(\n                      item.item!.targetReplyContent!,\n                      style: TextStyle(color: outline),\n                    ),\n                  ],\n                  const SizedBox(height: 4),\n                  Row(\n                    children: [\n                      Text(\n                        Utils.dateFormat(item.replyTime!, formatType: 'detail'),\n                        style: TextStyle(color: outline),\n                      ),\n                      const SizedBox(width: 16),\n                      // Text('回复', style: TextStyle(color: outline)),\n                    ],\n                  )\n                ],\n              ),\n            ),\n            const SizedBox(width: 25),\n            if (item.item!.type! == 'reply')\n              Container(\n                width: 60,\n                height: 80,\n                padding: const EdgeInsets.all(4),\n                child: Text(\n                  item.item!.rootReplyContent!,\n                  maxLines: 4,\n                  style: const TextStyle(fontSize: 12, letterSpacing: 0.3),\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n            if (item.item!.type! == 'video')\n              NetworkImgLayer(\n                width: 60,\n                height: 60,\n                src: item.item!.image,\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nInlineSpan buildContent(BuildContext context, item) {\n  List? atDetails = item!.atDetails;\n  final List<InlineSpan> spanChilds = <InlineSpan>[];\n  if (atDetails!.isNotEmpty) {\n    final String patternStr =\n        atDetails.map<String>((e) => '@${e['nickname']}').toList().join('|');\n    final RegExp regExp = RegExp(patternStr);\n    item.sourceContent!.splitMapJoin(\n      regExp,\n      onMatch: (Match match) {\n        spanChilds.add(\n          TextSpan(\n            text: match.group(0),\n            recognizer: TapGestureRecognizer()\n              ..onTap = () {\n                var currentUser = atDetails\n                    .where((e) => e['nickname'] == match.group(0)!.substring(1))\n                    .first;\n                Get.toNamed('/member?mid=${currentUser['mid']}', arguments: {\n                  'face': currentUser['avatar'],\n                });\n              },\n            style: TextStyle(color: Theme.of(context).colorScheme.primary),\n          ),\n        );\n        return '';\n      },\n      onNonMatch: (String nonMatch) {\n        spanChilds.add(\n          TextSpan(text: nonMatch),\n        );\n        return '';\n      },\n    );\n  } else {\n    spanChilds.add(\n      TextSpan(text: item.sourceContent),\n    );\n  }\n\n  return TextSpan(children: spanChilds);\n}\n"
  },
  {
    "path": "lib/pages/message/system/controller.dart",
    "content": "import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/msg.dart';\nimport 'package:pilipala/models/msg/system.dart';\n\nclass MessageSystemController extends GetxController {\n  RxList<MessageSystemModel> systemItems = <MessageSystemModel>[].obs;\n\n  Future<void> queryAndProcessMessages({String type = 'init'}) async {\n    // 并行调用两个接口\n    var results = await Future.wait([\n      queryMessageSystem(type: type),\n      queryMessageSystemAccount(type: type),\n    ]);\n\n    // 对返回的数据进行处理\n    var systemRes = results[0];\n    var accountRes = results[1];\n\n    if (systemRes['status'] || accountRes['status']) {\n      // 处理返回的数据\n      List<MessageSystemModel> combinedData = [\n        ...systemRes['data'],\n        ...accountRes['data']\n      ];\n      combinedData.sort((a, b) => b.cursor!.compareTo(a.cursor!));\n      systemItems.addAll(combinedData);\n      systemItems.refresh();\n      if (systemItems.isNotEmpty) {\n        systemMarkRead(systemItems.first.cursor!);\n      }\n    } else {\n      SmartDialog.showToast(systemRes['msg'] ?? accountRes['msg']);\n    }\n    return systemRes;\n  }\n\n  // 获取系统消息\n  Future queryMessageSystem({String type = 'init'}) async {\n    var res = await MsgHttp.messageSystem();\n    return res;\n  }\n\n  // 获取系统消息 个人\n  Future queryMessageSystemAccount({String type = 'init'}) async {\n    var res = await MsgHttp.messageSystemAccount();\n    return res;\n  }\n\n  // 标记已读\n  void systemMarkRead(int cursor) async {\n    await MsgHttp.systemMarkRead(cursor);\n  }\n}\n"
  },
  {
    "path": "lib/pages/message/system/index.dart",
    "content": "library message_system;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/message/system/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/models/msg/system.dart';\nimport 'controller.dart';\n\nclass MessageSystemPage extends StatefulWidget {\n  const MessageSystemPage({super.key});\n\n  @override\n  State<MessageSystemPage> createState() => _MessageSystemPageState();\n}\n\nclass _MessageSystemPageState extends State<MessageSystemPage> {\n  final MessageSystemController _messageSystemCtr =\n      Get.put(MessageSystemController());\n  late Future _futureBuilderFuture;\n  final ScrollController scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _messageSystemCtr.queryAndProcessMessages();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('系统通知'),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async {\n          await _messageSystemCtr.queryAndProcessMessages();\n        },\n        child: FutureBuilder(\n          future: _futureBuilderFuture,\n          builder: (BuildContext context, AsyncSnapshot snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              if (snapshot.data == null) {\n                return const SizedBox();\n              }\n              if (snapshot.data['status']) {\n                final systemItems = _messageSystemCtr.systemItems;\n                return Obx(\n                  () => ListView.separated(\n                    controller: scrollController,\n                    itemBuilder: (context, index) => SystemItem(\n                      item: systemItems[index],\n                      index: index,\n                      messageSystemCtr: _messageSystemCtr,\n                    ),\n                    itemCount: systemItems.length,\n                    separatorBuilder: (BuildContext context, int index) {\n                      return Divider(\n                        indent: 14,\n                        endIndent: 14,\n                        height: 1,\n                        color: Colors.grey.withOpacity(0.1),\n                      );\n                    },\n                  ),\n                );\n              } else {\n                // 请求错误\n                return CustomScrollView(\n                  slivers: [\n                    HttpError(\n                      errMsg: snapshot.data['msg'],\n                      fn: () {\n                        setState(() {\n                          _futureBuilderFuture =\n                              _messageSystemCtr.queryMessageSystem();\n                        });\n                      },\n                    )\n                  ],\n                );\n              }\n            } else {\n              return const SizedBox();\n            }\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass SystemItem extends StatelessWidget {\n  final MessageSystemModel item;\n  final int index;\n  final MessageSystemController messageSystemCtr;\n\n  const SystemItem(\n      {super.key,\n      required this.item,\n      required this.index,\n      required this.messageSystemCtr});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.fromLTRB(14, 14, 14, 12),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Text(item.title!,\n              style:\n                  const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),\n          const SizedBox(height: 4),\n          Text(\n            item.timeAt!,\n            style: TextStyle(color: Theme.of(context).colorScheme.outline),\n          ),\n          const SizedBox(height: 6),\n          Text(item.content is String ? item.content : item.content!['web']),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/mine/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/common/theme_type.dart';\nimport 'package:pilipala/models/user/info.dart';\nimport 'package:pilipala/models/user/stat.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass MineController extends GetxController {\n  // 用户信息 头像、昵称、lv\n  Rx<UserInfoData> userInfo = UserInfoData().obs;\n  // 用户状态 动态、关注、粉丝\n  Rx<UserStat> userStat = UserStat().obs;\n  RxBool userLogin = false.obs;\n  Box userInfoCache = GStrorage.userInfo;\n  Box setting = GStrorage.setting;\n  Rx<ThemeType> themeType = ThemeType.system.obs;\n\n  @override\n  onInit() {\n    super.onInit();\n\n    if (userInfoCache.get('userInfoCache') != null) {\n      userInfo.value = userInfoCache.get('userInfoCache');\n      userLogin.value = true;\n    }\n\n    themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,\n        defaultValue: ThemeType.system.code)];\n  }\n\n  onLogin() async {\n    if (!userLogin.value) {\n      Get.toNamed('/loginPage', preventDuplicates: false);\n    } else {\n      int mid = userInfo.value.mid!;\n      String face = userInfo.value.face!;\n      Get.toNamed(\n        '/member?mid=$mid',\n        arguments: {'face': face},\n      );\n    }\n  }\n\n  Future queryUserInfo() async {\n    if (!userLogin.value) {\n      return {'status': false};\n    }\n    var res = await UserHttp.userInfo();\n    if (res['status']) {\n      if (res['data'].isLogin) {\n        userInfo.value = res['data'];\n        userInfoCache.put('userInfoCache', res['data']);\n        userLogin.value = true;\n      } else {\n        resetUserInfo();\n      }\n    }\n    await queryUserStatOwner();\n    return res;\n  }\n\n  Future queryUserStatOwner() async {\n    var res = await UserHttp.userStatOwner();\n    if (res['status']) {\n      userStat.value = res['data'];\n    }\n    return res;\n  }\n\n  Future resetUserInfo() async {\n    userInfo.value = UserInfoData();\n    userStat.value = UserStat();\n    userInfoCache.delete('userInfoCache');\n    userLogin.value = false;\n  }\n\n  onChangeTheme() {\n    Brightness currentBrightness =\n        MediaQuery.of(Get.context!).platformBrightness;\n    ThemeType currentTheme = themeType.value;\n    switch (currentTheme) {\n      case ThemeType.dark:\n        setting.put(SettingBoxKey.themeMode, ThemeType.light.code);\n        themeType.value = ThemeType.light;\n        break;\n      case ThemeType.light:\n        setting.put(SettingBoxKey.themeMode, ThemeType.dark.code);\n        themeType.value = ThemeType.dark;\n        break;\n      case ThemeType.system:\n        // 判断当前的颜色模式\n        if (currentBrightness == Brightness.light) {\n          setting.put(SettingBoxKey.themeMode, ThemeType.dark.code);\n          themeType.value = ThemeType.dark;\n        } else {\n          setting.put(SettingBoxKey.themeMode, ThemeType.light.code);\n          themeType.value = ThemeType.light;\n        }\n        break;\n    }\n    Get.forceAppUpdate();\n  }\n\n  pushFollow() {\n    if (!userLogin.value) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    Get.toNamed('/follow?mid=${userInfo.value.mid}', preventDuplicates: false);\n  }\n\n  pushFans() {\n    if (!userLogin.value) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    Get.toNamed('/fan?mid=${userInfo.value.mid}', preventDuplicates: false);\n  }\n\n  pushDynamic() {\n    if (!userLogin.value) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}',\n        preventDuplicates: false);\n  }\n}\n"
  },
  {
    "path": "lib/pages/mine/index.dart",
    "content": "library mine;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/mine/view.dart",
    "content": "// ignore_for_file: no_leading_underscores_for_local_identifiers\n\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/common/theme_type.dart';\nimport 'package:pilipala/models/user/info.dart';\nimport 'controller.dart';\n\nclass MinePage extends StatefulWidget {\n  const MinePage({super.key});\n\n  @override\n  State<MinePage> createState() => _MinePageState();\n}\n\nclass _MinePageState extends State<MinePage> {\n  final MineController mineController = Get.put(MineController());\n  late Future _futureBuilderFuture;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = mineController.queryUserInfo();\n\n    mineController.userLogin.listen((status) {\n      if (mounted) {\n        setState(() {\n          _futureBuilderFuture = mineController.queryUserInfo();\n        });\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        automaticallyImplyLeading: false,\n        scrolledUnderElevation: 0,\n        elevation: 0,\n        toolbarHeight: kTextTabBarHeight + 20,\n        backgroundColor: Colors.transparent,\n        centerTitle: false,\n        title: const Text(\n          'PLPL',\n          style: TextStyle(\n            height: 2.8,\n            fontSize: 17,\n            fontWeight: FontWeight.bold,\n            fontFamily: 'Jura-Bold',\n          ),\n        ),\n        actions: [\n          IconButton(\n            onPressed: () => mineController.onChangeTheme(),\n            icon: Icon(\n              mineController.themeType.value == ThemeType.dark\n                  ? CupertinoIcons.sun_max\n                  : CupertinoIcons.moon,\n              size: 22,\n            ),\n          ),\n          IconButton(\n            onPressed: () => Get.toNamed('/setting', preventDuplicates: false),\n            icon: const Icon(\n              CupertinoIcons.slider_horizontal_3,\n            ),\n          ),\n          const SizedBox(width: 10),\n        ],\n      ),\n      body: LayoutBuilder(\n        builder: (context, constraint) {\n          return SingleChildScrollView(\n            physics: const NeverScrollableScrollPhysics(),\n            child: SizedBox(\n              height: constraint.maxHeight,\n              child: Column(\n                children: [\n                  const SizedBox(height: 10),\n                  FutureBuilder(\n                    future: _futureBuilderFuture,\n                    builder: (context, snapshot) {\n                      if (snapshot.connectionState == ConnectionState.done) {\n                        if (snapshot.data == null) {\n                          return const SizedBox();\n                        }\n                        if (snapshot.data['status']) {\n                          return Obx(\n                              () => userInfoBuild(mineController, context));\n                        } else {\n                          return userInfoBuild(mineController, context);\n                        }\n                      } else {\n                        return userInfoBuild(mineController, context);\n                      }\n                    },\n                  ),\n                ],\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget userInfoBuild(_mineController, context) {\n    return Column(\n      children: [\n        const SizedBox(height: 5),\n        GestureDetector(\n          onTap: () => _mineController.onLogin(),\n          child: ClipOval(\n            child: Container(\n              width: 85,\n              height: 85,\n              color: Theme.of(context).colorScheme.onInverseSurface,\n              child: Center(\n                child: _mineController.userInfo.value.face != null\n                    ? NetworkImgLayer(\n                        src: _mineController.userInfo.value.face,\n                        width: 85,\n                        height: 85)\n                    : Image.asset('assets/images/noface.jpeg'),\n              ),\n            ),\n          ),\n        ),\n        const SizedBox(height: 10),\n        Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Text(\n              _mineController.userInfo.value.uname ?? '点击头像登录',\n              style: Theme.of(context).textTheme.titleMedium,\n            ),\n            const SizedBox(width: 4),\n            Image.asset(\n              'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png',\n              height: 10,\n            ),\n          ],\n        ),\n        const SizedBox(height: 5),\n        Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Text.rich(TextSpan(children: [\n              TextSpan(\n                  text: '硬币: ',\n                  style:\n                      TextStyle(color: Theme.of(context).colorScheme.outline)),\n              TextSpan(\n                  text: (_mineController.userInfo.value.money ?? 'pilipala')\n                      .toString(),\n                  style:\n                      TextStyle(color: Theme.of(context).colorScheme.primary)),\n            ]))\n          ],\n        ),\n        const SizedBox(height: 25),\n        if (_mineController.userInfo.value.levelInfo != null) ...[\n          LayoutBuilder(\n            builder: (context, BoxConstraints box) {\n              LevelInfo levelInfo = _mineController.userInfo.value.levelInfo;\n              return SizedBox(\n                width: box.maxWidth,\n                height: 24,\n                child: Stack(\n                  children: [\n                    Positioned(\n                      top: 0,\n                      right: 0,\n                      bottom: 0,\n                      child: Container(\n                        color: Theme.of(context).colorScheme.primary,\n                        height: 24,\n                        constraints:\n                            const BoxConstraints(minWidth: 100), // 设置最小宽度为100\n                        width: box.maxWidth *\n                            (1 - (levelInfo.currentExp! / levelInfo.nextExp!)),\n                        child: Center(\n                          child: Text(\n                            '${levelInfo.currentExp!}/${levelInfo.nextExp!}',\n                            style: TextStyle(\n                              color: Theme.of(context).colorScheme.onPrimary,\n                              fontSize: 12,\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                    Positioned(\n                      top: 23,\n                      left: 0,\n                      bottom: 0,\n                      child: Container(\n                        width: box.maxWidth *\n                            (_mineController\n                                    .userInfo.value.levelInfo!.currentExp! /\n                                _mineController\n                                    .userInfo.value.levelInfo!.nextExp!),\n                        height: 1,\n                        decoration: BoxDecoration(\n                          color: Theme.of(context).colorScheme.primary,\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n              );\n            },\n          ),\n        ],\n        const SizedBox(height: 30),\n        Padding(\n          padding: const EdgeInsets.only(left: 12, right: 12),\n          child: LayoutBuilder(\n            builder: (context, constraints) {\n              TextStyle style = TextStyle(\n                  fontSize: Theme.of(context).textTheme.titleMedium!.fontSize,\n                  color: Theme.of(context).colorScheme.primary,\n                  fontWeight: FontWeight.bold);\n              return SizedBox(\n                height: constraints.maxWidth / 3 * 0.6,\n                child: GridView.count(\n                  primary: false,\n                  padding: const EdgeInsets.all(0),\n                  crossAxisCount: 3,\n                  childAspectRatio: 1.67,\n                  children: <Widget>[\n                    InkWell(\n                      onTap: () => _mineController.pushDynamic(),\n                      borderRadius: StyleString.mdRadius,\n                      child: Column(\n                        mainAxisAlignment: MainAxisAlignment.center,\n                        children: [\n                          AnimatedSwitcher(\n                            duration: const Duration(milliseconds: 400),\n                            transitionBuilder:\n                                (Widget child, Animation<double> animation) {\n                              return ScaleTransition(\n                                  scale: animation, child: child);\n                            },\n                            child: Text(\n                                (_mineController.userStat.value.dynamicCount ??\n                                        '-')\n                                    .toString(),\n                                key: ValueKey<String>(_mineController\n                                    .userStat.value.dynamicCount\n                                    .toString()),\n                                style: style),\n                          ),\n                          const SizedBox(height: 8),\n                          Text(\n                            '动态',\n                            style: Theme.of(context).textTheme.labelMedium,\n                          ),\n                        ],\n                      ),\n                    ),\n                    InkWell(\n                      onTap: () => _mineController.pushFollow(),\n                      borderRadius: StyleString.mdRadius,\n                      child: Column(\n                        mainAxisAlignment: MainAxisAlignment.center,\n                        children: [\n                          AnimatedSwitcher(\n                            duration: const Duration(milliseconds: 400),\n                            transitionBuilder:\n                                (Widget child, Animation<double> animation) {\n                              return ScaleTransition(\n                                  scale: animation, child: child);\n                            },\n                            child: Text(\n                                (_mineController.userStat.value.following ??\n                                        '-')\n                                    .toString(),\n                                key: ValueKey<String>(_mineController\n                                    .userStat.value.following\n                                    .toString()),\n                                style: style),\n                          ),\n                          const SizedBox(height: 8),\n                          Text(\n                            '关注',\n                            style: Theme.of(context).textTheme.labelMedium,\n                          ),\n                        ],\n                      ),\n                    ),\n                    InkWell(\n                      onTap: () => _mineController.pushFans(),\n                      borderRadius: StyleString.mdRadius,\n                      child: Column(\n                        mainAxisAlignment: MainAxisAlignment.center,\n                        children: [\n                          AnimatedSwitcher(\n                            duration: const Duration(milliseconds: 400),\n                            transitionBuilder:\n                                (Widget child, Animation<double> animation) {\n                              return ScaleTransition(\n                                  scale: animation, child: child);\n                            },\n                            child: Text(\n                                (_mineController.userStat.value.follower ?? '-')\n                                    .toString(),\n                                key: ValueKey<String>(_mineController\n                                    .userStat.value.follower\n                                    .toString()),\n                                style: style),\n                          ),\n                          const SizedBox(height: 8),\n                          Text(\n                            '粉丝',\n                            style: Theme.of(context).textTheme.labelMedium,\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              );\n            },\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass ActionItem extends StatelessWidget {\n  final Icon? icon;\n  final Function? onTap;\n  final String? text;\n\n  const ActionItem({\n    Key? key,\n    this.icon,\n    this.onTap,\n    this.text,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return InkWell(\n      onTap: () {},\n      borderRadius: StyleString.mdRadius,\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Icon(icon!.icon!),\n          const SizedBox(height: 8),\n          Text(\n            text!,\n            style: Theme.of(context).textTheme.labelMedium,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/opus/controller.dart",
    "content": "import 'dart:async';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/read.dart';\nimport 'package:pilipala/models/read/opus.dart';\nimport 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';\nimport 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';\n\nclass OpusController extends GetxController {\n  late String url;\n  RxString title = ''.obs;\n  late String id;\n  late String articleType;\n  Rx<OpusDataModel> opusData = OpusDataModel().obs;\n  final ScrollController scrollController = ScrollController();\n  late StreamController<bool> appbarStream = StreamController<bool>.broadcast();\n\n  @override\n  void onInit() {\n    super.onInit();\n    title.value = Get.parameters['title'] ?? '';\n    id = Get.parameters['id']!;\n    articleType = Get.parameters['articleType']!;\n    if (articleType == 'opus') {\n      url = 'https://www.bilibili.com/opus/$id';\n    }\n    scrollController.addListener(_scrollListener);\n  }\n\n  Future fetchOpusData() async {\n    var res = await ReadHttp.parseArticleOpus(id: id);\n    if (res['status']) {\n      List<String> keys = res.keys.toList();\n      if (keys.contains('isCv') && res['isCv']) {\n        Get.offNamed('/read', parameters: {\n          'id': res['cvId'],\n          'title': title.value,\n          'articleType': 'cv',\n        });\n      } else {\n        title.value = res['data'].detail!.basic!.title!;\n        opusData.value = res['data'];\n      }\n    }\n    return res;\n  }\n\n  void _scrollListener() {\n    final double offset = scrollController.position.pixels;\n    if (offset > 100) {\n      appbarStream.add(true);\n    } else {\n      appbarStream.add(false);\n    }\n  }\n\n  void onPreviewImg(picList, initIndex, context) {\n    Navigator.of(context).push(\n      HeroDialogRoute<void>(\n        builder: (BuildContext context) => InteractiveviewerGallery(\n          sources: picList,\n          initIndex: initIndex,\n          onPageChanged: (int pageIndex) {},\n        ),\n      ),\n    );\n  }\n\n  // 跳转webview\n  void onJumpWebview() {\n    Get.toNamed('/webview', parameters: {\n      'url': url,\n      'type': 'webview',\n      'pageTitle': title.value,\n    });\n  }\n\n  @override\n  void onClose() {\n    scrollController.removeListener(_scrollListener);\n    appbarStream.close();\n    super.onClose();\n  }\n}\n"
  },
  {
    "path": "lib/pages/opus/index.dart",
    "content": "library opus;\n\nexport 'controller.dart';\nexport 'view.dart';\n"
  },
  {
    "path": "lib/pages/opus/text_helper.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/models/read/opus.dart';\n\nclass TextHelper {\n  static Alignment getAlignment(int? align) {\n    switch (align) {\n      case 1:\n        return Alignment.center;\n      case 0:\n        return Alignment.centerLeft;\n      case 2:\n        return Alignment.centerRight;\n      default:\n        return Alignment.centerLeft;\n    }\n  }\n\n  static TextSpan buildTextSpan(\n      ModuleParagraphTextNode node, int? align, BuildContext context) {\n    // 获取node的所有key\n    if (node.nodeType != null) {\n      return TextSpan(\n        text: node.word?.words ?? '',\n        style: TextStyle(\n          fontSize:\n              node.word?.fontSize != null ? node.word!.fontSize! * 0.95 : 14,\n          fontWeight: node.word?.style?.bold != null\n              ? FontWeight.bold\n              : FontWeight.normal,\n          height: align == 1 ? 2 : 1.5,\n          color: node.word?.color != null\n              ? Color(int.parse(node.word!.color!.substring(1, 7), radix: 16) +\n                  0xFF000000)\n              : Theme.of(context).colorScheme.onBackground,\n        ),\n      );\n    } else {\n      switch (node.type) {\n        case 'TEXT_NODE_TYPE_WORD':\n          return TextSpan(\n            text: node.word?.words ?? '',\n            style: TextStyle(\n              fontSize: node.word?.fontSize != null\n                  ? node.word!.fontSize! * 0.95\n                  : 14,\n              fontWeight: node.word?.style?.bold != null\n                  ? FontWeight.bold\n                  : FontWeight.normal,\n              height: align == 1 ? 2 : 1.5,\n              color: node.word?.color != null\n                  ? Color(\n                      int.parse(node.word!.color!.substring(1, 7), radix: 16) +\n                          0xFF000000)\n                  : Theme.of(context).colorScheme.onBackground,\n            ),\n          );\n        default:\n          return const TextSpan(text: '');\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/opus/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/read/opus.dart';\nimport 'controller.dart';\nimport 'text_helper.dart';\n\nclass OpusPage extends StatefulWidget {\n  const OpusPage({super.key});\n\n  @override\n  State<OpusPage> createState() => _OpusPageState();\n}\n\nclass _OpusPageState extends State<OpusPage> {\n  final OpusController controller = Get.put(OpusController());\n  late Future _futureBuilderFuture;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = controller.fetchOpusData();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: _buildAppBar(),\n      body: SingleChildScrollView(\n        controller: controller.scrollController,\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.start,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            _buildTitle(),\n            _buildFutureContent(),\n          ],\n        ),\n      ),\n    );\n  }\n\n  AppBar _buildAppBar() {\n    return AppBar(\n      title: StreamBuilder(\n        stream: controller.appbarStream.stream.distinct(),\n        initialData: false,\n        builder: (BuildContext context, AsyncSnapshot snapshot) {\n          return AnimatedOpacity(\n            opacity: snapshot.data ? 1 : 0,\n            curve: Curves.easeOut,\n            duration: const Duration(milliseconds: 500),\n            child: Obx(\n              () => Text(\n                controller.title.value,\n                style: const TextStyle(fontSize: 16),\n              ),\n            ),\n          );\n        },\n      ),\n      actions: [\n        PopupMenuButton(\n          icon: const Icon(Icons.more_vert_outlined),\n          itemBuilder: (BuildContext context) => <PopupMenuEntry>[\n            PopupMenuItem(\n              onTap: controller.onJumpWebview,\n              child: const Text('查看原网页'),\n            )\n          ],\n        ),\n        const SizedBox(width: 16),\n      ],\n    );\n  }\n\n  Widget _buildTitle() {\n    return Padding(\n      padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),\n      child: Obx(\n        () => Text(\n          controller.title.value,\n          style: const TextStyle(\n            fontSize: 24,\n            fontWeight: FontWeight.bold,\n            letterSpacing: 1,\n            height: 1.5,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildFutureContent() {\n    return FutureBuilder(\n      future: _futureBuilderFuture,\n      builder: (BuildContext context, AsyncSnapshot snapshot) {\n        if (snapshot.connectionState == ConnectionState.done) {\n          if (snapshot.data == null) {\n            return const SizedBox();\n          }\n          if (snapshot.data['status']) {\n            return _buildContent(controller.opusData.value);\n          } else {\n            return _buildError(snapshot.data['message']);\n          }\n        } else {\n          return _buildLoading();\n        }\n      },\n    );\n  }\n\n  Widget _buildContent(OpusDataModel opusData) {\n    if (opusData.detail == null) {\n      return const SizedBox();\n    }\n    final modules = opusData.detail!.modules!;\n    late ModuleContent moduleContent;\n    // 获取所有的图片链接\n    final List<String> picList = [];\n    final int moduleIndex =\n        modules.indexWhere((module) => module.moduleContent != null);\n    if (moduleIndex != -1) {\n      moduleContent = modules[moduleIndex].moduleContent!;\n      for (var paragraph in moduleContent.paragraphs!) {\n        if (paragraph.paraType == 2) {\n          for (var pic in paragraph.pic!.pics!) {\n            picList.add(pic.url!);\n          }\n        }\n      }\n    } else {\n      print('No moduleContent found');\n    }\n\n    return Padding(\n      padding: EdgeInsets.fromLTRB(\n          16, 0, 16, MediaQuery.of(context).padding.bottom + 40),\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.start,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Padding(\n            padding: const EdgeInsets.only(bottom: 30),\n            child: _buildStatsWidget(opusData),\n          ),\n          Padding(\n            padding: const EdgeInsets.only(bottom: 20),\n            child: _buildAuthorWidget(opusData),\n          ),\n          ...moduleContent.paragraphs!.map(\n            (ModuleParagraph paragraph) {\n              return Column(\n                children: [\n                  if (paragraph.paraType == 1) ...[\n                    Container(\n                      alignment: TextHelper.getAlignment(paragraph.align),\n                      margin: const EdgeInsets.only(bottom: 10),\n                      child: SelectableText.rich(\n                        TextSpan(\n                          children: paragraph.text?.nodes?.map((node) {\n                                return TextHelper.buildTextSpan(\n                                    node, paragraph.align, context);\n                              }).toList() ??\n                              [],\n                        ),\n                      ),\n                    )\n                  ] else if (paragraph.paraType == 2) ...[\n                    ...paragraph.pic?.pics?.map(\n                          (Pic pic) => Center(\n                            child: Padding(\n                              padding:\n                                  const EdgeInsets.only(top: 10, bottom: 10),\n                              child: InkWell(\n                                onTap: () {\n                                  controller.onPreviewImg(\n                                    picList,\n                                    picList.indexOf(pic.url!),\n                                    context,\n                                  );\n                                },\n                                child: NetworkImgLayer(\n                                  src: pic.url,\n                                  width: (Get.size.width - 32) * pic.scale!,\n                                  height: (Get.size.width - 32) *\n                                      pic.scale! /\n                                      pic.aspectRatio!,\n                                  type: 'emote',\n                                ),\n                              ),\n                            ),\n                          ),\n                        ) ??\n                        [],\n                  ] else\n                    const SizedBox(),\n                ],\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildAuthorWidget(OpusDataModel opusData) {\n    final modules = opusData.detail!.modules!;\n    late ModuleAuthor moduleAuthor;\n    final int moduleIndex =\n        modules.indexWhere((module) => module.moduleAuthor != null);\n    if (moduleIndex != -1) {\n      moduleAuthor = modules[moduleIndex].moduleAuthor!;\n    } else {\n      return const SizedBox();\n    }\n    return Row(\n      children: [\n        NetworkImgLayer(\n          width: 48,\n          height: 48,\n          type: 'avatar',\n          src: moduleAuthor.face,\n        ),\n        const SizedBox(width: 10),\n        Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              moduleAuthor.name!,\n              style: const TextStyle(\n                fontWeight: FontWeight.w500,\n              ),\n            ),\n            StyledText(moduleAuthor.pubTime!),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget _buildStatsWidget(OpusDataModel opusData) {\n    final modules = opusData.detail!.modules!;\n    final ModuleStat moduleStat = modules.last.moduleStat!;\n    return Row(\n      children: [\n        StyledText('${moduleStat.comment!.count}评论'),\n        const SizedBox(width: 10),\n        StyledText('${moduleStat.like!.count}赞'),\n        const SizedBox(width: 10),\n        StyledText('${moduleStat.favorite!.count}转发'),\n      ],\n    );\n  }\n\n  Widget _buildError(String message) {\n    return SizedBox(\n      height: 100,\n      child: Center(\n        child: Text(message),\n      ),\n    );\n  }\n\n  Widget _buildLoading() {\n    return const SizedBox(\n      height: 100,\n      child: Center(\n        child: CircularProgressIndicator(),\n      ),\n    );\n  }\n}\n\nclass StyledText extends StatelessWidget {\n  final String text;\n\n  const StyledText(this.text, {Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      text,\n      style: TextStyle(\n        fontSize: 13,\n        color: Theme.of(context).colorScheme.outline,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/rank/controller.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/rank_type.dart';\nimport 'package:pilipala/pages/rank/zone/index.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass RankController extends GetxController with GetTickerProviderStateMixin {\n  bool flag = false;\n  late RxList tabs = [].obs;\n  RxInt initialIndex = 0.obs;\n  late TabController tabController;\n  late List tabsCtrList;\n  late List<Widget> tabsPageList;\n  Box setting = GStrorage.setting;\n  late final StreamController<bool> searchBarStream =\n      StreamController<bool>.broadcast();\n\n  @override\n  void onInit() {\n    super.onInit();\n    // 进行tabs配置\n    setTabConfig();\n  }\n\n  void onRefresh() {\n    int index = tabController.index;\n    final ZoneController ctr = tabsCtrList[index];\n    ctr.onRefresh();\n  }\n\n  void animateToTop() {\n    int index = tabController.index;\n    final ZoneController ctr = tabsCtrList[index];\n    ctr.animateToTop();\n  }\n\n  void setTabConfig() async {\n    tabs.value = tabsConfig;\n    initialIndex.value = 0;\n    tabsCtrList = tabs\n        .map((e) => Get.put(ZoneController(), tag: e['rid'].toString()))\n        .toList();\n    tabsPageList = tabs.map<Widget>((e) => e['page']).toList();\n\n    tabController = TabController(\n      initialIndex: initialIndex.value,\n      length: tabs.length,\n      vsync: this,\n    );\n  }\n\n  @override\n  void onClose() {\n    searchBarStream.close();\n    super.onClose();\n  }\n}\n"
  },
  {
    "path": "lib/pages/rank/index.dart",
    "content": "library rank;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/rank/view.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport './controller.dart';\n\nclass RankPage extends StatefulWidget {\n  const RankPage({Key? key}) : super(key: key);\n\n  @override\n  State<RankPage> createState() => _RankPageState();\n}\n\nclass _RankPageState extends State<RankPage>\n    with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {\n  final RankController _rankController = Get.put(RankController());\n  List videoList = [];\n  late Stream<bool> stream;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    stream = _rankController.searchBarStream.stream;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return Scaffold(\n      extendBody: true,\n      extendBodyBehindAppBar: true,\n      backgroundColor: Colors.transparent,\n      appBar: AppBar(\n        toolbarHeight: 0,\n        elevation: 0,\n        backgroundColor: Colors.transparent,\n        systemOverlayStyle: Platform.isAndroid\n            ? SystemUiOverlayStyle(\n                statusBarIconBrightness:\n                    Theme.of(context).brightness == Brightness.dark\n                        ? Brightness.light\n                        : Brightness.dark,\n              )\n            : Theme.of(context).brightness == Brightness.dark\n                ? SystemUiOverlayStyle.light\n                : SystemUiOverlayStyle.dark,\n      ),\n      body: Column(\n        children: [\n          const CustomAppBar(),\n          if (_rankController.tabs.length > 1) ...[\n            const SizedBox(height: 4),\n            SizedBox(\n              width: double.infinity,\n              height: 42,\n              child: Align(\n                alignment: Alignment.center,\n                child: TabBar(\n                  controller: _rankController.tabController,\n                  tabs: [\n                    for (var i in _rankController.tabs) Tab(text: i['label'])\n                  ],\n                  isScrollable: true,\n                  dividerColor: Colors.transparent,\n                  enableFeedback: true,\n                  splashBorderRadius: BorderRadius.circular(10),\n                  tabAlignment: TabAlignment.center,\n                  onTap: (value) {\n                    feedBack();\n                    if (_rankController.initialIndex.value == value) {\n                      _rankController.tabsCtrList[value].animateToTop();\n                    }\n                    _rankController.initialIndex.value = value;\n                  },\n                ),\n              ),\n            ),\n          ] else ...[\n            const SizedBox(height: 6),\n          ],\n          Expanded(\n            child: TabBarView(\n              controller: _rankController.tabController,\n              children: _rankController.tabsPageList,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass CustomAppBar extends StatelessWidget implements PreferredSizeWidget {\n  final double height;\n\n  const CustomAppBar({\n    super.key,\n    this.height = kToolbarHeight,\n  });\n\n  @override\n  Size get preferredSize => Size.fromHeight(height);\n\n  @override\n  Widget build(BuildContext context) {\n    final double top = MediaQuery.of(context).padding.top;\n    return Container(\n      width: MediaQuery.of(context).size.width,\n      height: top,\n      color: Colors.transparent,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/rank/zone/controller.dart",
    "content": "import 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/model_hot_video_item.dart';\n\nclass ZoneController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  RxList<HotVideoItemModel> videoList = <HotVideoItemModel>[].obs;\n  bool isLoadingMore = false;\n  bool flag = false;\n  OverlayEntry? popupDialog;\n  int zoneID = 0;\n\n  // 获取推荐\n  Future queryRankFeed(type, rid) async {\n    zoneID = rid;\n    var res = await VideoHttp.getRankVideoList(zoneID);\n    if (res['status']) {\n      if (type == 'init') {\n        videoList.value = res['data'];\n      } else if (type == 'onRefresh') {\n        videoList.clear();\n        videoList.addAll(res['data']);\n      } else if (type == 'onLoad') {\n        videoList.clear();\n        videoList.addAll(res['data']);\n      }\n    }\n    isLoadingMore = false;\n    return res;\n  }\n\n  // 下拉刷新\n  Future onRefresh() async {\n    queryRankFeed('onRefresh', zoneID);\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    queryRankFeed('onLoad', zoneID);\n  }\n\n  // 返回顶部并刷新\n  void animateToTop() async {\n    if (scrollController.hasClients) {\n      if (scrollController.offset >=\n          MediaQuery.of(Get.context!).size.height * 5) {\n        scrollController.jumpTo(0);\n      } else {\n        await scrollController.animateTo(0,\n            duration: const Duration(milliseconds: 500),\n            curve: Curves.easeInOut);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/rank/zone/index.dart",
    "content": "library rank.zone;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/rank/zone/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/video_card_h.dart';\nimport 'package:pilipala/pages/rank/zone/index.dart';\nimport 'package:pilipala/utils/main_stream.dart';\n\nclass ZonePage extends StatefulWidget {\n  const ZonePage({Key? key, required this.rid}) : super(key: key);\n\n  final int rid;\n\n  @override\n  State<ZonePage> createState() => _ZonePageState();\n}\n\nclass _ZonePageState extends State<ZonePage>\n    with AutomaticKeepAliveClientMixin {\n  late ZoneController _zoneController;\n  List videoList = [];\n  Future? _futureBuilderFuture;\n  late ScrollController scrollController;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    _zoneController = Get.put(ZoneController(), tag: widget.rid.toString());\n    _futureBuilderFuture = _zoneController.queryRankFeed('init', widget.rid);\n    scrollController = _zoneController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          if (!_zoneController.isLoadingMore) {\n            _zoneController.isLoadingMore = true;\n            _zoneController.onLoad();\n          }\n        }\n        handleScrollEvent(scrollController);\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return RefreshIndicator(\n      onRefresh: () async {\n        return await _zoneController.onRefresh();\n      },\n      child: CustomScrollView(\n        controller: _zoneController.scrollController,\n        slivers: [\n          SliverPadding(\n            // 单列布局 EdgeInsets.zero\n            padding:\n                const EdgeInsets.fromLTRB(0, StyleString.safeSpace - 5, 0, 0),\n            sliver: FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  Map data = snapshot.data as Map;\n                  if (data['status']) {\n                    return Obx(\n                      () => SliverList(\n                        delegate: SliverChildBuilderDelegate((context, index) {\n                          return VideoCardH(\n                            videoItem: _zoneController.videoList[index],\n                            showPubdate: true,\n                          );\n                        }, childCount: _zoneController.videoList.length),\n                      ),\n                    );\n                  } else {\n                    return HttpError(\n                      errMsg: data['msg'],\n                      fn: () {\n                        setState(() {\n                          _futureBuilderFuture =\n                              _zoneController.queryRankFeed('init', widget.rid);\n                        });\n                      },\n                    );\n                  }\n                } else {\n                  // 骨架屏\n                  return SliverList(\n                    delegate: SliverChildBuilderDelegate((context, index) {\n                      return const VideoCardHSkeleton();\n                    }, childCount: 10),\n                  );\n                }\n              },\n            ),\n          ),\n          SliverToBoxAdapter(\n            child: SizedBox(\n              height: MediaQuery.of(context).padding.bottom + 10,\n            ),\n          )\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/rcmd/controller.dart",
    "content": "import 'package:flutter/cupertino.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/home/rcmd/result.dart';\nimport 'package:pilipala/models/model_rec_video_item.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass RcmdController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  int _currentPage = 0;\n  // RxList<RecVideoItemAppModel> appVideoList = <RecVideoItemAppModel>[].obs;\n  // RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs;\n  bool isLoadingMore = true;\n  OverlayEntry? popupDialog;\n  Box setting = GStrorage.setting;\n  RxInt crossAxisCount = 2.obs;\n  late bool enableSaveLastData;\n  late String defaultRcmdType = 'web';\n  late RxList<dynamic> videoList;\n\n  @override\n  void onInit() {\n    super.onInit();\n    crossAxisCount.value =\n        setting.get(SettingBoxKey.customRows, defaultValue: 2);\n    enableSaveLastData =\n        setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false);\n    defaultRcmdType =\n        setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');\n    if (defaultRcmdType == 'web') {\n      videoList = <RecVideoItemModel>[].obs;\n    } else {\n      videoList = <RecVideoItemAppModel>[].obs;\n    }\n  }\n\n  // 获取推荐\n  Future queryRcmdFeed(type) async {\n    if (isLoadingMore == false) {\n      return;\n    }\n    if (type == 'onRefresh') {\n      _currentPage = 0;\n    }\n    late final Map<String, dynamic> res;\n    switch (defaultRcmdType) {\n      case 'app':\n      case 'notLogin':\n        res = await VideoHttp.rcmdVideoListApp(\n          loginStatus: defaultRcmdType != 'notLogin',\n          freshIdx: _currentPage,\n        );\n        break;\n      default: //'web'\n        res = await VideoHttp.rcmdVideoList(\n          freshIdx: _currentPage,\n          ps: 20,\n        );\n    }\n    if (res['status']) {\n      if (type == 'init') {\n        if (videoList.isNotEmpty) {\n          videoList.addAll(res['data']);\n        } else {\n          videoList.value = res['data'];\n        }\n      } else if (type == 'onRefresh') {\n        if (enableSaveLastData) {\n          videoList.insertAll(0, res['data']);\n        } else {\n          videoList.value = res['data'];\n        }\n      } else if (type == 'onLoad') {\n        videoList.addAll(res['data']);\n      }\n      _currentPage += 1;\n      // 若videoList数量太小，可能会影响翻页，此时再次请求\n      // 为避免请求到的数据太少时还在反复请求，要求本次返回数据大于1条才触发\n      if (res['data'].length > 1 && videoList.length < 10) {\n        queryRcmdFeed('onLoad');\n      }\n    }\n    isLoadingMore = false;\n    return res;\n  }\n\n  // 下拉刷新\n  Future onRefresh() async {\n    isLoadingMore = true;\n    queryRcmdFeed('onRefresh');\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    queryRcmdFeed('onLoad');\n  }\n\n  // 返回顶部\n  void animateToTop() async {\n    if (scrollController.offset >=\n        MediaQuery.of(Get.context!).size.height * 5) {\n      scrollController.jumpTo(0);\n    } else {\n      await scrollController.animateTo(0,\n          duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);\n    }\n  }\n\n  void blockUserCb(mid) {\n    videoList.removeWhere((e) => e.owner.mid == mid);\n    videoList.refresh();\n    SmartDialog.showToast('已移除相关视频');\n  }\n}\n"
  },
  {
    "path": "lib/pages/rcmd/index.dart",
    "content": "library recm_panel;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/rcmd/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/skeleton/video_card_v.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/video_card_v.dart';\nimport 'package:pilipala/utils/main_stream.dart';\n\nimport 'controller.dart';\n\nclass RcmdPage extends StatefulWidget {\n  const RcmdPage({super.key});\n\n  @override\n  State<RcmdPage> createState() => _RcmdPageState();\n}\n\nclass _RcmdPageState extends State<RcmdPage>\n    with AutomaticKeepAliveClientMixin {\n  final RcmdController _rcmdController = Get.put(RcmdController());\n  late Future _futureBuilderFuture;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _rcmdController.queryRcmdFeed('init');\n    ScrollController scrollController = _rcmdController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle(\n              'my-throttler', const Duration(milliseconds: 200), () {\n            _rcmdController.isLoadingMore = true;\n            _rcmdController.onLoad();\n          });\n        }\n        handleScrollEvent(scrollController);\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _rcmdController.scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return Container(\n      clipBehavior: Clip.hardEdge,\n      margin: const EdgeInsets.only(\n          left: StyleString.safeSpace, right: StyleString.safeSpace),\n      decoration: const BoxDecoration(\n        borderRadius: BorderRadius.all(StyleString.imgRadius),\n      ),\n      child: RefreshIndicator(\n        onRefresh: () async {\n          await _rcmdController.onRefresh();\n          await Future.delayed(const Duration(milliseconds: 300));\n        },\n        child: CustomScrollView(\n          controller: _rcmdController.scrollController,\n          physics: const AlwaysScrollableScrollPhysics(),\n          slivers: [\n            SliverPadding(\n              padding:\n                  const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),\n              sliver: FutureBuilder(\n                future: _futureBuilderFuture,\n                builder: (context, snapshot) {\n                  if (snapshot.connectionState == ConnectionState.done) {\n                    Map data = snapshot.data as Map;\n                    if (data['status']) {\n                      return Obx(\n                        () {\n                          if (_rcmdController.isLoadingMore &&\n                              _rcmdController.videoList.isEmpty) {\n                            return contentGrid(_rcmdController, []);\n                          } else {\n                            // 显示视频列表\n                            return contentGrid(\n                                _rcmdController, _rcmdController.videoList);\n                          }\n                        },\n                      );\n                    } else {\n                      return HttpError(\n                        errMsg: data['msg'],\n                        fn: () {\n                          setState(() {\n                            _rcmdController.isLoadingMore = true;\n                            _futureBuilderFuture =\n                                _rcmdController.queryRcmdFeed('init');\n                          });\n                        },\n                      );\n                    }\n                  } else {\n                    return contentGrid(_rcmdController, []);\n                  }\n                },\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget contentGrid(ctr, videoList) {\n    // double maxWidth = Get.size.width;\n    // int baseWidth = 500;\n    // int step = 300;\n    // int crossAxisCount =\n    //     maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;\n    // if (maxWidth < 300) {\n    //   crossAxisCount = 1;\n    // }\n    int crossAxisCount = ctr.crossAxisCount.value;\n    double mainAxisExtent = (Get.size.width /\n            crossAxisCount /\n            StyleString.aspectRatio) +\n        (crossAxisCount == 1 ? 68 : MediaQuery.textScalerOf(context).scale(86));\n    return SliverGrid(\n      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n        // 行间距\n        mainAxisSpacing: StyleString.safeSpace,\n        // 列间距\n        crossAxisSpacing: StyleString.safeSpace,\n        // 列数\n        crossAxisCount: crossAxisCount,\n        mainAxisExtent: mainAxisExtent,\n      ),\n      delegate: SliverChildBuilderDelegate(\n        (BuildContext context, int index) {\n          return videoList!.isNotEmpty\n              ? VideoCardV(\n                  videoItem: videoList[index],\n                  crossAxisCount: crossAxisCount,\n                  blockUserCb: (mid) => ctr.blockUserCb(mid),\n                )\n              : const VideoCardVSkeleton();\n        },\n        childCount: videoList!.isNotEmpty ? videoList!.length : 10,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/read/controller.dart",
    "content": "import 'dart:async';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/read.dart';\nimport 'package:pilipala/models/read/read.dart';\nimport 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';\nimport 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';\n\nclass ReadPageController extends GetxController {\n  late String url;\n  RxString title = ''.obs;\n  late String id;\n  late String articleType;\n  Rx<ReadDataModel> cvData = ReadDataModel().obs;\n  final ScrollController scrollController = ScrollController();\n  late StreamController<bool> appbarStream = StreamController<bool>.broadcast();\n\n  @override\n  void onInit() {\n    super.onInit();\n    title.value = Get.parameters['title'] ?? '';\n    id = Get.parameters['id']!;\n    articleType = Get.parameters['articleType'] ?? 'read';\n    url = 'https://www.bilibili.com/read/cv$id';\n    scrollController.addListener(_scrollListener);\n    fetchViewInfo();\n  }\n\n  Future fetchCvData() async {\n    var res = await ReadHttp.parseArticleCv(id: id);\n    if (res['status']) {\n      cvData.value = res['data'];\n      title.value = cvData.value.readInfo!.title!;\n    }\n    return res;\n  }\n\n  void _scrollListener() {\n    final double offset = scrollController.position.pixels;\n    if (offset > 100) {\n      appbarStream.add(true);\n    } else {\n      appbarStream.add(false);\n    }\n  }\n\n  void onPreviewImg(picList, initIndex, context) {\n    Navigator.of(context).push(\n      HeroDialogRoute<void>(\n        builder: (BuildContext context) => InteractiveviewerGallery(\n          sources: picList,\n          initIndex: initIndex,\n          onPageChanged: (int pageIndex) {},\n        ),\n      ),\n    );\n  }\n\n  void fetchViewInfo() {\n    ReadHttp.getViewInfo(id: id);\n  }\n\n  // 跳转webview\n  void onJumpWebview() {\n    Get.toNamed('/webview', parameters: {\n      'url': url,\n      'type': 'webview',\n      'pageTitle': title.value,\n    });\n  }\n\n  @override\n  void onClose() {\n    scrollController.removeListener(_scrollListener);\n    appbarStream.close();\n    super.onClose();\n  }\n}\n"
  },
  {
    "path": "lib/pages/read/index.dart",
    "content": "library read;\n\nexport 'controller.dart';\nexport 'view.dart';\n"
  },
  {
    "path": "lib/pages/read/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/html_render.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/read/opus.dart';\nimport 'package:pilipala/models/read/read.dart';\nimport 'package:pilipala/pages/opus/text_helper.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'controller.dart';\n\nclass ReadPage extends StatefulWidget {\n  const ReadPage({super.key});\n\n  @override\n  State<ReadPage> createState() => _ReadPageState();\n}\n\nclass _ReadPageState extends State<ReadPage> {\n  final ReadPageController controller = Get.put(ReadPageController());\n  late Future _futureBuilderFuture;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = controller.fetchCvData();\n  }\n\n  List<String> extractDataSrc(String input) {\n    final regex = RegExp(r'data-src=\"([^\"]*)\"');\n    final matches = regex.allMatches(input);\n    return matches.map((match) {\n      final dataSrc = match.group(1)!;\n      return dataSrc.startsWith('//') ? 'https:$dataSrc' : dataSrc;\n    }).toList();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: _buildAppBar(),\n      body: SingleChildScrollView(\n        controller: controller.scrollController,\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.start,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            _buildTitle(),\n            _buildFutureContent(),\n          ],\n        ),\n      ),\n    );\n  }\n\n  AppBar _buildAppBar() {\n    return AppBar(\n      title: StreamBuilder(\n        stream: controller.appbarStream.stream.distinct(),\n        initialData: false,\n        builder: (BuildContext context, AsyncSnapshot snapshot) {\n          return AnimatedOpacity(\n            opacity: snapshot.data ? 1 : 0,\n            curve: Curves.easeOut,\n            duration: const Duration(milliseconds: 500),\n            child: Obx(\n              () => Text(\n                controller.title.value,\n                style: const TextStyle(fontSize: 16),\n              ),\n            ),\n          );\n        },\n      ),\n      actions: [\n        PopupMenuButton(\n          icon: const Icon(Icons.more_vert_outlined),\n          itemBuilder: (BuildContext context) => <PopupMenuEntry>[\n            PopupMenuItem(\n              onTap: controller.onJumpWebview,\n              child: const Text('查看原网页'),\n            )\n          ],\n        ),\n        const SizedBox(width: 16),\n      ],\n    );\n  }\n\n  Widget _buildTitle() {\n    return Padding(\n      padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),\n      child: Obx(\n        () => Text(\n          controller.title.value,\n          style: const TextStyle(\n            fontSize: 24,\n            fontWeight: FontWeight.bold,\n            letterSpacing: 1,\n            height: 1.5,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildFutureContent() {\n    return FutureBuilder(\n      future: _futureBuilderFuture,\n      builder: (BuildContext context, AsyncSnapshot snapshot) {\n        if (snapshot.connectionState == ConnectionState.done) {\n          if (snapshot.data == null) {\n            return const SizedBox();\n          }\n          if (snapshot.data['status']) {\n            return _buildContent(snapshot.data['data']);\n          } else {\n            return _buildError(snapshot.data['message']);\n          }\n        } else {\n          return _buildLoading();\n        }\n      },\n    );\n  }\n\n  Widget _buildContent(ReadDataModel cvData) {\n    final List<String> picList = _extractPicList(cvData);\n    final List<String> imgList = extractDataSrc(cvData.readInfo!.content!);\n    return Padding(\n      padding: EdgeInsets.fromLTRB(\n          16, 0, 16, MediaQuery.of(context).padding.bottom + 40),\n      child: cvData.readInfo!.opus == null\n          ? _buildNonOpusContent(cvData, imgList)\n          : _buildOpusContent(cvData, picList),\n    );\n  }\n\n  List<String> _extractPicList(ReadDataModel cvData) {\n    final List<String> picList = [];\n    if (cvData.readInfo!.opus != null) {\n      final List<ModuleParagraph> paragraphs =\n          cvData.readInfo!.opus!.content!.paragraphs!;\n      for (var paragraph in paragraphs) {\n        if (paragraph.paraType == 2) {\n          for (var pic in paragraph.pic!.pics!) {\n            picList.add(pic.url!);\n          }\n        }\n      }\n    }\n    return picList;\n  }\n\n  Widget _buildNonOpusContent(ReadDataModel cvData, List<String> imgList) {\n    return Column(\n      children: [\n        Padding(\n          padding: const EdgeInsets.only(bottom: 30),\n          child: _buildStatsWidget(cvData),\n        ),\n        Padding(\n          padding: const EdgeInsets.only(bottom: 20),\n          child: _buildAuthorWidget(cvData),\n        ),\n        SelectionArea(\n          child: HtmlRender(\n            htmlContent: cvData.readInfo!.content!,\n            imgList: imgList,\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildOpusContent(ReadDataModel cvData, List<String> picList) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.start,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.only(bottom: 30),\n          child: _buildStatsWidget(cvData),\n        ),\n        Padding(\n          padding: const EdgeInsets.only(bottom: 20),\n          child: _buildAuthorWidget(cvData),\n        ),\n        ...cvData.readInfo!.opus!.content!.paragraphs!.map(\n          (ModuleParagraph paragraph) {\n            return Column(\n              children: [\n                if (paragraph.paraType == 1)\n                  _buildTextParagraph(paragraph)\n                else if (paragraph.paraType == 2)\n                  ..._buildPics(paragraph, picList)\n                else\n                  const SizedBox(),\n              ],\n            );\n          },\n        ),\n      ],\n    );\n  }\n\n  Widget _buildTextParagraph(ModuleParagraph paragraph) {\n    return Container(\n      alignment: TextHelper.getAlignment(paragraph.align),\n      margin: const EdgeInsets.only(bottom: 10),\n      child: SelectableText.rich(\n        TextSpan(\n          children: paragraph.text?.nodes?.map((node) {\n                return TextHelper.buildTextSpan(node, paragraph.align, context);\n              }).toList() ??\n              [],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildError(String message) {\n    return SizedBox(\n      height: 100,\n      child: Center(\n        child: Text(message),\n      ),\n    );\n  }\n\n  Widget _buildLoading() {\n    return const SizedBox(\n      height: 100,\n      child: Center(\n        child: CircularProgressIndicator(),\n      ),\n    );\n  }\n\n  Widget _buildStatsWidget(ReadDataModel cvData) {\n    return Row(\n      children: [\n        StyledText(Utils.CustomStamp_str(\n          timestamp: cvData.readInfo!.publishTime!,\n          date: 'YY-MM-DD hh:mm',\n          toInt: false,\n        )),\n        const SizedBox(width: 10),\n        StyledText('${Utils.numFormat(cvData.readInfo!.stats!['view'])}浏览'),\n        const StyledText(' · '),\n        StyledText('${cvData.readInfo!.stats!['like']}点赞'),\n        // const StyledText(' · '),\n        // StyledText('${cvData.readInfo!.stats!['reply']}评论'),\n      ],\n    );\n  }\n\n  Widget _buildAuthorWidget(ReadDataModel cvData) {\n    final Author author = cvData.readInfo!.author!;\n    return Row(\n      children: [\n        NetworkImgLayer(\n          width: 48,\n          height: 48,\n          type: 'avatar',\n          src: author.face,\n        ),\n        const SizedBox(width: 10),\n        Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              children: [\n                Text(\n                  author.name!,\n                  style: TextStyle(\n                    color: author.vip!.nicknameColor != null\n                        ? Color(author.vip!.nicknameColor!)\n                        : null,\n                    fontWeight: FontWeight.w500,\n                  ),\n                ),\n                const SizedBox(width: 6),\n                Image.asset(\n                  'assets/images/lv/lv${author.level}.png',\n                  height: 11,\n                ),\n              ],\n            ),\n            Row(\n              children: [\n                StyledText('粉丝: ${Utils.numFormat(author.fans)}'),\n                const SizedBox(width: 10),\n                StyledText(\n                    '文章: ${Utils.numFormat(cvData.readInfo!.totalArtNum)}'),\n              ],\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  List<Widget> _buildPics(ModuleParagraph paragraph, List<String> picList) {\n    return paragraph.pic?.pics\n            ?.map(\n              (Pic pic) => Center(\n                child: Padding(\n                  padding: const EdgeInsets.only(top: 10, bottom: 10),\n                  child: InkWell(\n                    onTap: () {\n                      controller.onPreviewImg(\n                        picList,\n                        picList.indexOf(pic.url!),\n                        context,\n                      );\n                    },\n                    child: NetworkImgLayer(\n                      src: pic.url,\n                      width: (Get.size.width - 32) * pic.scale!,\n                      height:\n                          (Get.size.width - 32) * pic.scale! / pic.aspectRatio!,\n                      type: 'emote',\n                    ),\n                  ),\n                ),\n              ),\n            )\n            .toList() ??\n        [];\n  }\n}\n\nclass StyledText extends StatelessWidget {\n  final String text;\n\n  const StyledText(this.text, {Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      text,\n      style: TextStyle(\n        fontSize: 13,\n        color: Theme.of(context).colorScheme.outline,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/search/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/search/hot.dart';\nimport 'package:pilipala/models/search/suggest.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass SSearchController extends GetxController {\n  final FocusNode searchFocusNode = FocusNode();\n  RxString searchKeyWord = ''.obs;\n  Rx<TextEditingController> controller = TextEditingController().obs;\n  RxList<HotSearchItem> hotSearchList = <HotSearchItem>[].obs;\n  Box histiryWord = GStrorage.historyword;\n  List historyCacheList = [];\n  RxList historyList = [].obs;\n  RxList<SearchSuggestItem> searchSuggestList = <SearchSuggestItem>[].obs;\n  final _debouncer =\n      Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间\n  String hintText = '搜索';\n  RxString defaultSearch = ''.obs;\n  Box setting = GStrorage.setting;\n  bool enableHotKey = true;\n\n  @override\n  void onInit() {\n    super.onInit();\n    // 其他页面跳转过来\n    if (Get.parameters.keys.isNotEmpty) {\n      if (Get.parameters['keyword'] != null) {\n        onClickKeyword(Get.parameters['keyword']!);\n      }\n      if (Get.parameters['hintText'] != null) {\n        hintText = Get.parameters['hintText']!;\n        searchKeyWord.value = hintText;\n      }\n    }\n    historyCacheList = histiryWord.get('cacheList') ?? [];\n    historyList.value = historyCacheList;\n    enableHotKey = setting.get(SettingBoxKey.enableHotKey, defaultValue: true);\n  }\n\n  void onChange(value) {\n    searchKeyWord.value = value;\n    if (value == '') {\n      searchSuggestList.value = [];\n      return;\n    }\n    _debouncer.call(() => querySearchSuggest(value));\n  }\n\n  void onClear() {\n    if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {\n      controller.value.clear();\n      searchKeyWord.value = '';\n      searchSuggestList.value = [];\n    } else {\n      Get.back();\n    }\n  }\n\n  // 搜索\n  void submit() {\n    // ignore: unrelated_type_equality_checks\n    if (searchKeyWord == '') {\n      return;\n    }\n    List arr = historyCacheList.where((e) => e != searchKeyWord.value).toList();\n    arr.insert(0, searchKeyWord.value);\n    historyCacheList = arr;\n\n    historyList.value = historyCacheList;\n    // 手动刷新\n    historyList.refresh();\n    histiryWord.put('cacheList', historyCacheList);\n    searchFocusNode.unfocus();\n    Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value});\n  }\n\n  // 获取热搜关键词\n  Future queryHotSearchList() async {\n    var result = await SearchHttp.hotSearchList();\n    if (result['status']) {\n      hotSearchList.value = result['data'].list;\n    }\n    return result;\n  }\n\n  // 点击热搜关键词\n  void onClickKeyword(String keyword) {\n    searchKeyWord.value = keyword;\n    controller.value.text = keyword;\n    // 移动光标\n    controller.value.selection = TextSelection.fromPosition(\n      TextPosition(offset: controller.value.text.length),\n    );\n    submit();\n  }\n\n  Future querySearchSuggest(String value) async {\n    var result = await SearchHttp.searchSuggest(term: value);\n    if (result['status']) {\n      if (result['data'] is SearchSuggestModel) {\n        searchSuggestList.value = result['data'].tag;\n      }\n    }\n  }\n\n  onSelect(word) {\n    searchKeyWord.value = word;\n    controller.value.text = word;\n    submit();\n  }\n\n  onLongSelect(word) {\n    int index = historyList.indexOf(word);\n    historyList.removeAt(index);\n    historyList.refresh();\n    histiryWord.put('cacheList', historyList);\n  }\n\n  onClearHis() {\n    historyList.value = [];\n    historyCacheList = [];\n    historyList.refresh();\n    histiryWord.put('cacheList', []);\n  }\n}\n"
  },
  {
    "path": "lib/pages/search/index.dart",
    "content": "library search;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/search/view.dart",
    "content": "import 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'controller.dart';\nimport 'widgets/hot_keyword.dart';\nimport 'widgets/search_text.dart';\n\nclass SearchPage extends StatefulWidget {\n  const SearchPage({super.key});\n\n  @override\n  State<SearchPage> createState() => _SearchPageState();\n  static final RouteObserver<PageRoute> routeObserver =\n      RouteObserver<PageRoute>();\n}\n\nclass _SearchPageState extends State<SearchPage> with RouteAware {\n  final SSearchController _searchController = Get.put(SSearchController());\n  late Future? _futureBuilderFuture;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _searchController.queryHotSearchList();\n  }\n\n  @override\n  // 返回当前页面时\n  void didPopNext() async {\n    _searchController.searchFocusNode.requestFocus();\n    super.didPopNext();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    SearchPage.routeObserver\n        .subscribe(this, ModalRoute.of(context) as PageRoute);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      resizeToAvoidBottomInset: false,\n      appBar: AppBar(\n        shape: Border(\n          bottom: BorderSide(\n            color: Theme.of(context).dividerColor.withOpacity(0.08),\n            width: 1,\n          ),\n        ),\n        titleSpacing: 0,\n        actions: [\n          IconButton(\n            onPressed: () => _searchController.submit(),\n            icon: const Icon(CupertinoIcons.search, size: 22),\n          ),\n          const SizedBox(width: 10)\n        ],\n        title: Obx(\n          () => TextField(\n            autofocus: true,\n            focusNode: _searchController.searchFocusNode,\n            controller: _searchController.controller.value,\n            textInputAction: TextInputAction.search,\n            onChanged: (value) => _searchController.onChange(value),\n            decoration: InputDecoration(\n              hintText: _searchController.hintText,\n              border: InputBorder.none,\n              suffixIcon: IconButton(\n                icon: Icon(\n                  Icons.clear,\n                  size: 22,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n                onPressed: () => _searchController.onClear(),\n              ),\n            ),\n            onSubmitted: (String value) => _searchController.submit(),\n          ),\n        ),\n      ),\n      body: SingleChildScrollView(\n        child: Column(\n          children: [\n            const SizedBox(height: 12),\n            // 搜索建议\n            _searchSuggest(),\n            // 热搜\n            Visibility(\n              visible: _searchController.enableHotKey,\n              child: hotSearch(_searchController),\n            ),\n            // 搜索历史\n            _history()\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _searchSuggest() {\n    SSearchController ssCtr = _searchController;\n    return Obx(\n      () => ssCtr.searchSuggestList.isNotEmpty &&\n              ssCtr.searchSuggestList.first.term != null &&\n              ssCtr.controller.value.text != ''\n          ? ListView.builder(\n              physics: const NeverScrollableScrollPhysics(),\n              shrinkWrap: true,\n              itemCount: ssCtr.searchSuggestList.length,\n              itemBuilder: (context, index) {\n                return InkWell(\n                  customBorder: RoundedRectangleBorder(\n                    borderRadius: BorderRadius.circular(4),\n                  ),\n                  onTap: () => ssCtr\n                      .onClickKeyword(ssCtr.searchSuggestList[index].term!),\n                  child: Padding(\n                    padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),\n                    child: ssCtr.searchSuggestList[index].textRich,\n                  ),\n                );\n              },\n            )\n          : const SizedBox(),\n    );\n  }\n\n  Widget hotSearch(ctr) {\n    return Padding(\n      padding: const EdgeInsets.fromLTRB(10, 14, 4, 0),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Padding(\n            padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),\n            child: Row(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                Text(\n                  '大家都在搜',\n                  style: Theme.of(context)\n                      .textTheme\n                      .titleMedium!\n                      .copyWith(fontWeight: FontWeight.bold),\n                ),\n                SizedBox(\n                  height: 34,\n                  child: TextButton.icon(\n                    style: ButtonStyle(\n                      padding: MaterialStateProperty.all(const EdgeInsets.only(\n                          left: 10, top: 6, bottom: 6, right: 10)),\n                    ),\n                    onPressed: () => ctr.queryHotSearchList(),\n                    icon: const Icon(Icons.refresh_outlined, size: 18),\n                    label: const Text('刷新'),\n                  ),\n                ),\n              ],\n            ),\n          ),\n          LayoutBuilder(\n            builder: (context, boxConstraints) {\n              final double width = boxConstraints.maxWidth;\n              return FutureBuilder(\n                future: _futureBuilderFuture,\n                builder: (context, snapshot) {\n                  if (snapshot.connectionState == ConnectionState.done) {\n                    if (snapshot.data == null) {\n                      return const SizedBox();\n                    }\n                    Map data = snapshot.data as Map;\n                    if (data['status']) {\n                      return Obx(\n                        () => HotKeyword(\n                          width: width,\n                          // ignore: invalid_use_of_protected_member\n                          hotSearchList: _searchController.hotSearchList.value,\n                          onClick: (keyword) async {\n                            _searchController.searchFocusNode.unfocus();\n                            await Future.delayed(\n                                const Duration(milliseconds: 150));\n                            _searchController.onClickKeyword(keyword);\n                          },\n                        ),\n                      );\n                    } else {\n                      return CustomScrollView(\n                        slivers: [\n                          HttpError(\n                            errMsg: data['msg'],\n                            fn: () => setState(() {}),\n                          )\n                        ],\n                      );\n                    }\n                  } else {\n                    // 缓存数据\n                    if (_searchController.hotSearchList.isNotEmpty) {\n                      return HotKeyword(\n                        width: width,\n                        hotSearchList: _searchController.hotSearchList,\n                      );\n                    } else {\n                      return const SizedBox();\n                    }\n                  }\n                },\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _history() {\n    return Obx(\n      () => Container(\n        width: double.infinity,\n        padding: const EdgeInsets.fromLTRB(10, 25, 6, 0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (_searchController.historyList.isNotEmpty)\n              Padding(\n                padding: const EdgeInsets.fromLTRB(6, 0, 0, 2),\n                child: Row(\n                  mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                  children: [\n                    Text(\n                      '搜索历史',\n                      style: Theme.of(context)\n                          .textTheme\n                          .titleMedium!\n                          .copyWith(fontWeight: FontWeight.bold),\n                    ),\n                    TextButton(\n                      onPressed: () => _searchController.onClearHis(),\n                      child: const Text('清空'),\n                    )\n                  ],\n                ),\n              ),\n            Obx(\n              () => Wrap(\n                spacing: 8,\n                runSpacing: 8,\n                direction: Axis.horizontal,\n                textDirection: TextDirection.ltr,\n                children: [\n                  for (int i = 0; i < _searchController.historyList.length; i++)\n                    SearchText(\n                      searchText: _searchController.historyList[i],\n                      searchTextIdx: i,\n                      onSelect: (value) => _searchController.onSelect(value),\n                      onLongSelect: (value) =>\n                          _searchController.onLongSelect(value),\n                    )\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/search/widgets/hot_keyword.dart",
    "content": "// ignore: file_names\nimport 'package:cached_network_image/cached_network_image.dart';\nimport 'package:flutter/material.dart';\n\nclass HotKeyword extends StatelessWidget {\n  final double? width;\n  final List? hotSearchList;\n  final Function? onClick;\n  const HotKeyword({\n    this.width,\n    this.hotSearchList,\n    this.onClick,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Wrap(\n      runSpacing: 0.4,\n      spacing: 5.0,\n      children: [\n        for (var i in hotSearchList!)\n          SizedBox(\n            width: width! / 2 - 4,\n            child: Material(\n              borderRadius: BorderRadius.circular(3),\n              clipBehavior: Clip.hardEdge,\n              child: InkWell(\n                onTap: () => onClick!(i.keyword),\n                child: Padding(\n                  padding: EdgeInsets.only(\n                      left: 2,\n                      right: hotSearchList!.indexOf(i) % 2 == 1 ? 10 : 0),\n                  child: Row(\n                    children: [\n                      Flexible(\n                        child: Padding(\n                          padding: const EdgeInsets.fromLTRB(6, 5, 4, 5),\n                          child: Text(\n                            i.keyword!,\n                            overflow: TextOverflow.ellipsis,\n                            maxLines: 1,\n                            style: const TextStyle(fontSize: 14),\n                          ),\n                        ),\n                      ),\n                      if (i.icon != null && i.icon != '')\n                        SizedBox(\n                          height: 15,\n                          child: CachedNetworkImage(\n                              imageUrl: i.icon!, height: 15.0),\n                        ),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/search/widgets/search_text.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass SearchText extends StatelessWidget {\n  final String? searchText;\n  final Function? onSelect;\n  final int? searchTextIdx;\n  final Function? onLongSelect;\n  final bool isSelect;\n  const SearchText({\n    super.key,\n    this.searchText,\n    this.onSelect,\n    this.searchTextIdx,\n    this.onLongSelect,\n    this.isSelect = false,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: isSelect\n          ? Theme.of(context).colorScheme.primaryContainer\n          : Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),\n      borderRadius: BorderRadius.circular(6),\n      child: Padding(\n        padding: EdgeInsets.zero,\n        child: InkWell(\n          onTap: () {\n            onSelect!(searchText);\n          },\n          onLongPress: () {\n            onLongSelect!(searchText);\n          },\n          borderRadius: BorderRadius.circular(6),\n          child: Padding(\n            padding:\n                const EdgeInsets.only(top: 5, bottom: 5, left: 11, right: 11),\n            child: Text(\n              searchText!,\n              style: TextStyle(\n                color: isSelect\n                    ? Theme.of(context).colorScheme.primary\n                    : Theme.of(context).colorScheme.onSurfaceVariant,\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/search_panel/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass SearchPanelController extends GetxController {\n  SearchPanelController({this.keyword, this.searchType});\n  ScrollController scrollController = ScrollController();\n  String? keyword;\n  SearchType? searchType;\n  RxInt page = 1.obs;\n  RxList resultList = [].obs;\n  // 结果排序方式 搜索类型为视频、专栏及相簿时\n  RxString order = ''.obs;\n  // 视频时长筛选 仅用于搜索视频\n  RxInt duration = 0.obs;\n  // 视频分区筛选 仅用于搜索视频 -1时不传\n  RxInt tids = (-1).obs;\n\n  Future onSearch({type = 'init'}) async {\n    var result = await SearchHttp.searchByType(\n      searchType: searchType!,\n      keyword: keyword!,\n      page: page.value,\n      order: !['video', 'article'].contains(searchType!.type)\n          ? null\n          : (order.value == '' ? null : order.value),\n      duration: searchType!.type != 'video' ? null : duration.value,\n      tids: searchType!.type != 'video' ? null : tids.value,\n    );\n    if (result['status']) {\n      if (type == 'onRefresh') {\n        resultList.value = result['data'].list ?? [];\n      } else {\n        resultList.addAll(result['data'].list ?? []);\n      }\n      page.value++;\n      onPushDetail(keyword, resultList);\n    }\n    return result;\n  }\n\n  Future onRefresh() async {\n    page.value = 1;\n    await onSearch(type: 'onRefresh');\n  }\n\n  // 返回顶部并刷新\n  void animateToTop() async {\n    if (scrollController.offset >=\n        MediaQuery.of(Get.context!).size.height * 5) {\n      scrollController.jumpTo(0);\n    } else {\n      await scrollController.animateTo(0,\n          duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);\n    }\n  }\n\n  void onPushDetail(keyword, resultList) async {\n    // 匹配输入内容，如果是AV、BV号且有结果 直接跳转详情页\n    Map matchRes = IdUtils.matchAvorBv(input: keyword);\n    List matchKeys = matchRes.keys.toList();\n    String? bvid;\n    try {\n      bvid = resultList.first.bvid;\n    } catch (_) {\n      bvid = null;\n    }\n    // keyword 可能输入纯数字\n    int? aid;\n    try {\n      aid = resultList.first.aid;\n    } catch (_) {\n      aid = null;\n    }\n    if (matchKeys.isNotEmpty && searchType == SearchType.video ||\n        aid.toString() == keyword) {\n      String heroTag = Utils.makeHeroTag(bvid);\n      int cid = await SearchHttp.ab2c(aid: aid, bvid: bvid);\n      if (matchKeys.isNotEmpty &&\n              matchKeys.first == 'BV' &&\n              matchRes[matchKeys.first] == bvid ||\n          matchKeys.isNotEmpty &&\n              matchKeys.first == 'AV' &&\n              matchRes[matchKeys.first] == aid ||\n          aid.toString() == keyword) {\n        Get.toNamed(\n          '/video?bvid=$bvid&cid=$cid',\n          arguments: {'videoItem': resultList.first, 'heroTag': heroTag},\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/search_panel/index.dart",
    "content": "library searchpanel;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/search_panel/view.dart",
    "content": "// ignore_for_file: invalid_use_of_protected_member\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/media_bangumi.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/models/common/search_type.dart';\n\nimport 'controller.dart';\nimport 'widgets/article_panel.dart';\nimport 'widgets/live_panel.dart';\nimport 'widgets/media_bangumi_panel.dart';\nimport 'widgets/user_panel.dart';\nimport 'widgets/video_panel.dart';\n\nclass SearchPanel extends StatefulWidget {\n  final String? keyword;\n  final SearchType? searchType;\n  final String? tag;\n  const SearchPanel(\n      {required this.keyword, required this.searchType, this.tag, Key? key})\n      : super(key: key);\n\n  @override\n  State<SearchPanel> createState() => _SearchPanelState();\n}\n\nclass _SearchPanelState extends State<SearchPanel>\n    with AutomaticKeepAliveClientMixin {\n  late SearchPanelController _searchPanelController;\n\n  late Future _futureBuilderFuture;\n  late ScrollController scrollController;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    _searchPanelController = Get.put(\n      SearchPanelController(\n        keyword: widget.keyword,\n        searchType: widget.searchType,\n      ),\n      tag: widget.searchType!.type + widget.keyword!,\n    );\n\n    /// 专栏默认排序\n    if (widget.searchType == SearchType.article) {\n      _searchPanelController.order.value = 'totalrank';\n    }\n    scrollController = _searchPanelController.scrollController;\n    scrollController.addListener(() async {\n      if (scrollController.position.pixels >=\n          scrollController.position.maxScrollExtent - 100) {\n        EasyThrottle.throttle('history', const Duration(seconds: 1), () {\n          _searchPanelController.onSearch(type: 'onLoad');\n        });\n      }\n    });\n    _futureBuilderFuture = _searchPanelController.onSearch();\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return RefreshIndicator(\n      onRefresh: () async {\n        await _searchPanelController.onRefresh();\n      },\n      child: FutureBuilder(\n        future: _futureBuilderFuture,\n        builder: (context, snapshot) {\n          if (snapshot.connectionState == ConnectionState.done) {\n            if (snapshot.data != null) {\n              Map data = snapshot.data;\n              var ctr = _searchPanelController;\n              RxList list = ctr.resultList;\n              if (data['status']) {\n                return Obx(() {\n                  switch (widget.searchType) {\n                    case SearchType.video:\n                      return SearchVideoPanel(\n                        ctr: _searchPanelController,\n                        list: list.value,\n                      );\n                    case SearchType.media_bangumi:\n                      return searchMbangumiPanel(context, ctr, list);\n                    case SearchType.bili_user:\n                      return searchUserPanel(context, ctr, list);\n                    case SearchType.live_room:\n                      return searchLivePanel(context, ctr, list);\n                    case SearchType.article:\n                      return SearchArticlePanel(\n                        ctr: _searchPanelController,\n                        list: list.value,\n                      );\n                    default:\n                      return const SizedBox();\n                  }\n                });\n              } else {\n                return CustomScrollView(\n                  physics: const NeverScrollableScrollPhysics(),\n                  slivers: [\n                    HttpError(\n                      errMsg: data['msg'],\n                      fn: () {\n                        setState(() {\n                          _searchPanelController.onSearch();\n                        });\n                      },\n                    ),\n                  ],\n                );\n              }\n            } else {\n              return CustomScrollView(\n                physics: const NeverScrollableScrollPhysics(),\n                slivers: [\n                  HttpError(\n                    errMsg: '没有相关数据',\n                    fn: () {\n                      setState(() {\n                        _searchPanelController.onSearch();\n                      });\n                    },\n                  ),\n                ],\n              );\n            }\n          } else {\n            // 骨架屏\n            return ListView.builder(\n              addAutomaticKeepAlives: false,\n              addRepaintBoundaries: false,\n              itemCount: 15,\n              itemBuilder: (context, index) {\n                switch (widget.searchType) {\n                  case SearchType.video:\n                    return const VideoCardHSkeleton();\n                  case SearchType.media_bangumi:\n                    return const MediaBangumiSkeleton();\n                  case SearchType.bili_user:\n                    return const VideoCardHSkeleton();\n                  case SearchType.live_room:\n                    return const VideoCardHSkeleton();\n                  default:\n                    return const VideoCardHSkeleton();\n                }\n              },\n            );\n          }\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/search_panel/widgets/article_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/pages/search_panel/index.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass SearchArticlePanel extends StatelessWidget {\n  SearchArticlePanel({\n    required this.ctr,\n    this.list,\n    Key? key,\n  }) : super(key: key);\n\n  final SearchPanelController ctr;\n  final List? list;\n\n  final ArticlePanelController controller = Get.put(ArticlePanelController());\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      alignment: Alignment.topCenter,\n      children: [\n        searchArticlePanel(context, ctr, list),\n        Container(\n          width: double.infinity,\n          height: 36,\n          padding: const EdgeInsets.only(left: 8, top: 0, right: 8),\n          child: Row(\n            children: [\n              Expanded(\n                child: SingleChildScrollView(\n                  scrollDirection: Axis.horizontal,\n                  child: Obx(\n                    () => Wrap(\n                      // spacing: ,\n                      children: [\n                        for (var i in controller.filterList) ...[\n                          CustomFilterChip(\n                            label: i['label'],\n                            type: i['type'],\n                            selectedType: controller.selectedType.value,\n                            callFn: (bool selected) async {\n                              controller.selectedType.value = i['type'];\n                              ctr.order.value =\n                                  i['type'].toString().split('.').last;\n                              SmartDialog.showLoading(msg: 'loading');\n                              await ctr.onRefresh();\n                              SmartDialog.dismiss();\n                            },\n                          ),\n                        ]\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nWidget searchArticlePanel(BuildContext context, ctr, list) {\n  TextStyle textStyle = TextStyle(\n      fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,\n      color: Theme.of(context).colorScheme.outline);\n  return Padding(\n    padding: const EdgeInsets.only(top: 36),\n    child: list!.isNotEmpty\n        ? ListView.builder(\n            controller: ctr!.scrollController,\n            addAutomaticKeepAlives: false,\n            addRepaintBoundaries: false,\n            itemCount: list.length,\n            itemBuilder: (context, index) {\n              return InkWell(\n                onTap: () {\n                  Get.toNamed('/read', parameters: {\n                    'title': list[index].subTitle,\n                    'id': list[index].id.toString(),\n                    'articleType': 'read'\n                  });\n                },\n                child: Padding(\n                  padding: const EdgeInsets.fromLTRB(\n                      StyleString.safeSpace, 5, StyleString.safeSpace, 5),\n                  child: LayoutBuilder(builder: (context, boxConstraints) {\n                    final double width = (boxConstraints.maxWidth -\n                            StyleString.cardSpace *\n                                6 /\n                                MediaQuery.textScalerOf(context).scale(1.0)) /\n                        2;\n                    return Container(\n                      constraints: const BoxConstraints(minHeight: 88),\n                      height: width / StyleString.aspectRatio,\n                      child: Row(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: <Widget>[\n                          if (list[index].imageUrls != null &&\n                              list[index].imageUrls.isNotEmpty)\n                            AspectRatio(\n                              aspectRatio: StyleString.aspectRatio,\n                              child: LayoutBuilder(\n                                  builder: (context, boxConstraints) {\n                                double maxWidth = boxConstraints.maxWidth;\n                                double maxHeight = boxConstraints.maxHeight;\n                                return NetworkImgLayer(\n                                  width: maxWidth,\n                                  height: maxHeight,\n                                  src: list[index].imageUrls.first,\n                                );\n                              }),\n                            ),\n                          Expanded(\n                            child: Padding(\n                              padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),\n                              child: Column(\n                                mainAxisSize: MainAxisSize.min,\n                                crossAxisAlignment: CrossAxisAlignment.start,\n                                children: [\n                                  RichText(\n                                    maxLines: 2,\n                                    text: TextSpan(\n                                      children: [\n                                        for (var i in list[index].title) ...[\n                                          TextSpan(\n                                            text: i['text'],\n                                            style: TextStyle(\n                                              fontWeight: FontWeight.w500,\n                                              letterSpacing: 0.3,\n                                              color: i['type'] == 'em'\n                                                  ? Theme.of(context)\n                                                      .colorScheme\n                                                      .primary\n                                                  : Theme.of(context)\n                                                      .colorScheme\n                                                      .onSurface,\n                                            ),\n                                          ),\n                                        ]\n                                      ],\n                                    ),\n                                  ),\n                                  const Spacer(),\n                                  Text(\n                                      Utils.dateFormat(list[index].pubTime,\n                                          formatType: 'detail'),\n                                      style: textStyle),\n                                  Row(\n                                    children: [\n                                      Text('${list[index].view}浏览',\n                                          style: textStyle),\n                                      Text(' • ', style: textStyle),\n                                      Text('${list[index].reply}评论',\n                                          style: textStyle),\n                                    ],\n                                  ),\n                                ],\n                              ),\n                            ),\n                          ),\n                        ],\n                      ),\n                    );\n                  }),\n                ),\n              );\n            },\n          )\n        : CustomScrollView(\n            slivers: [\n              HttpError(\n                errMsg: '没有数据',\n                isShowBtn: false,\n                fn: () => {},\n              )\n            ],\n          ),\n  );\n}\n\nclass CustomFilterChip extends StatelessWidget {\n  const CustomFilterChip({\n    this.label,\n    this.type,\n    this.selectedType,\n    this.callFn,\n    Key? key,\n  }) : super(key: key);\n\n  final String? label;\n  final ArticleFilterType? type;\n  final ArticleFilterType? selectedType;\n  final Function? callFn;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 34,\n      child: FilterChip(\n        padding: const EdgeInsets.only(left: 11, right: 11),\n        labelPadding: EdgeInsets.zero,\n        label: Text(\n          label!,\n          style: const TextStyle(fontSize: 13),\n        ),\n        labelStyle: TextStyle(\n            color: type == selectedType\n                ? Theme.of(context).colorScheme.primary\n                : Theme.of(context).colorScheme.outline),\n        selected: type == selectedType,\n        showCheckmark: false,\n        shape: ContinuousRectangleBorder(\n          borderRadius: BorderRadius.circular(12),\n        ),\n        selectedColor: Colors.transparent,\n        // backgroundColor:\n        //     Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),\n        backgroundColor: Colors.transparent,\n        side: BorderSide.none,\n        onSelected: (bool selected) => callFn!(selected),\n      ),\n    );\n  }\n}\n\nclass ArticlePanelController extends GetxController {\n  RxList<Map> filterList = [{}].obs;\n  Rx<ArticleFilterType> selectedType = ArticleFilterType.values.first.obs;\n\n  @override\n  void onInit() {\n    List<Map<String, dynamic>> list = ArticleFilterType.values\n        .map((type) => {\n              'label': type.description,\n              'type': type,\n            })\n        .toList();\n    filterList.value = list;\n    super.onInit();\n  }\n}\n"
  },
  {
    "path": "lib/pages/search_panel/widgets/live_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/image_save.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nWidget searchLivePanel(BuildContext context, ctr, list) {\n  return Padding(\n    padding: const EdgeInsets.only(\n        left: StyleString.safeSpace, right: StyleString.safeSpace),\n    child: GridView.builder(\n      primary: false,\n      controller: ctr!.scrollController,\n      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n          crossAxisCount: 2,\n          crossAxisSpacing: StyleString.cardSpace + 2,\n          mainAxisSpacing: StyleString.cardSpace + 3,\n          mainAxisExtent:\n              MediaQuery.sizeOf(context).width / 2 / StyleString.aspectRatio +\n                  MediaQuery.textScalerOf(context).scale(66.0)),\n      itemCount: list.length,\n      itemBuilder: (context, index) {\n        return LiveItem(liveItem: list![index]);\n      },\n    ),\n  );\n}\n\nclass LiveItem extends StatelessWidget {\n  final dynamic liveItem;\n  const LiveItem({Key? key, required this.liveItem}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(liveItem.roomid);\n    return Card(\n      elevation: 1,\n      clipBehavior: Clip.hardEdge,\n      margin: EdgeInsets.zero,\n      child: InkWell(\n        onTap: () async {\n          Get.toNamed('/liveRoom?roomid=${liveItem.roomid}',\n              arguments: {'liveItem': liveItem, 'heroTag': heroTag});\n        },\n        onLongPress: () => imageSaveDialog(\n          context,\n          liveItem,\n          SmartDialog.dismiss,\n        ),\n        child: Column(\n          children: [\n            ClipRRect(\n              borderRadius: const BorderRadius.all(StyleString.imgRadius),\n              child: AspectRatio(\n                aspectRatio: StyleString.aspectRatio,\n                child: LayoutBuilder(builder: (context, boxConstraints) {\n                  double maxWidth = boxConstraints.maxWidth;\n                  double maxHeight = boxConstraints.maxHeight;\n                  return Stack(\n                    children: [\n                      Hero(\n                        tag: heroTag,\n                        child: NetworkImgLayer(\n                          src: liveItem.cover,\n                          type: 'emote',\n                          width: maxWidth,\n                          height: maxHeight,\n                        ),\n                      ),\n                      Positioned(\n                        left: 0,\n                        right: 0,\n                        bottom: 0,\n                        child: AnimatedOpacity(\n                          opacity: 1,\n                          duration: const Duration(milliseconds: 200),\n                          child: LiveStat(\n                            online: liveItem.online,\n                            cateName: liveItem.cateName,\n                          ),\n                        ),\n                      ),\n                    ],\n                  );\n                }),\n              ),\n            ),\n            LiveContent(liveItem: liveItem)\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass LiveContent extends StatelessWidget {\n  final dynamic liveItem;\n  const LiveContent({Key? key, required this.liveItem}) : super(key: key);\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(9, 8, 9, 6),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            RichText(\n              text: TextSpan(\n                children: [\n                  for (var i in liveItem.titleList) ...[\n                    TextSpan(\n                      text: i['text'],\n                      style: TextStyle(\n                        fontWeight: FontWeight.w500,\n                        letterSpacing: 0.3,\n                        color: i['type'] == 'em'\n                            ? Theme.of(context).colorScheme.primary\n                            : Theme.of(context).colorScheme.onSurface,\n                      ),\n                    ),\n                  ]\n                ],\n              ),\n            ),\n            SizedBox(\n              width: double.infinity,\n              child: Text(\n                liveItem.uname,\n                maxLines: 1,\n                style: TextStyle(\n                  fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass LiveStat extends StatelessWidget {\n  final int? online;\n  final String? cateName;\n\n  const LiveStat({Key? key, required this.online, this.cateName})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 45,\n      padding: const EdgeInsets.only(top: 22, left: 8, right: 8),\n      decoration: const BoxDecoration(\n        gradient: LinearGradient(\n          begin: Alignment.topCenter,\n          end: Alignment.bottomCenter,\n          colors: <Color>[\n            Colors.transparent,\n            Colors.black54,\n          ],\n          tileMode: TileMode.mirror,\n        ),\n      ),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          Text(\n            cateName!,\n            style: const TextStyle(fontSize: 11, color: Colors.white),\n          ),\n          Text(\n            '围观:${online.toString()}',\n            style: const TextStyle(fontSize: 11, color: Colors.white),\n          )\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/search_panel/widgets/media_bangumi_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/bangumi/info.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nWidget searchMbangumiPanel(BuildContext context, ctr, list) {\n  TextStyle style =\n      TextStyle(fontSize: Theme.of(context).textTheme.labelMedium!.fontSize);\n  return ListView.builder(\n    controller: ctr!.scrollController,\n    addAutomaticKeepAlives: false,\n    addRepaintBoundaries: false,\n    itemCount: list!.length,\n    itemBuilder: (context, index) {\n      var i = list![index];\n      return InkWell(\n        onTap: () {\n          /// TODO 番剧详情页面\n          // Get.toNamed('/video?bvid=${i.bvid}&cid=${i.cid}', arguments: {\n          //   'videoItem': i,\n          //   'heroTag': Utils.makeHeroTag(i.id),\n          //   'videoType': SearchType.media_bangumi\n          // });\n        },\n        child: Padding(\n          padding: const EdgeInsets.fromLTRB(\n              StyleString.safeSpace, 7, StyleString.safeSpace, 7),\n          child: Row(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Stack(\n                children: [\n                  NetworkImgLayer(\n                    width: 111,\n                    height: 148,\n                    src: i.cover,\n                  ),\n                  PBadge(\n                    text: i.mediaType == 1 ? '番剧' : '国创',\n                    top: 6.0,\n                    right: 4.0,\n                    bottom: null,\n                    left: null,\n                  )\n                ],\n              ),\n              const SizedBox(width: 10),\n              Expanded(\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    const SizedBox(height: 4),\n                    RichText(\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                      text: TextSpan(\n                        style: TextStyle(\n                            color: Theme.of(context).colorScheme.onSurface),\n                        children: [\n                          for (var i in i.titleList) ...[\n                            TextSpan(\n                              text: i['text'],\n                              style: TextStyle(\n                                fontSize: MediaQuery.textScalerOf(context)\n                                    .scale(Theme.of(context)\n                                        .textTheme\n                                        .titleSmall!\n                                        .fontSize!),\n                                fontWeight: FontWeight.bold,\n                                color: i['type'] == 'em'\n                                    ? Theme.of(context).colorScheme.primary\n                                    : Theme.of(context).colorScheme.onSurface,\n                              ),\n                            ),\n                          ],\n                        ],\n                      ),\n                    ),\n                    const SizedBox(height: 12),\n                    Text('评分:${i.mediaScore['score'].toString()}',\n                        style: style),\n                    Row(\n                      children: [\n                        Text(i.areas, style: style),\n                        const SizedBox(width: 3),\n                        const Text('·'),\n                        const SizedBox(width: 3),\n                        Text(Utils.dateFormat(i.pubtime).toString(),\n                            style: style),\n                      ],\n                    ),\n                    Row(\n                      children: [\n                        Text(i.styles, style: style),\n                        const SizedBox(width: 3),\n                        const Text('·'),\n                        const SizedBox(width: 3),\n                        Text(i.indexShow, style: style),\n                      ],\n                    ),\n                    const SizedBox(height: 18),\n                    SizedBox(\n                      height: 32,\n                      child: ElevatedButton(\n                        onPressed: () {\n                          RoutePush.bangumiPush(i.seasonId, null);\n                        },\n                        child: const Text('观看'),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/pages/search_panel/widgets/user_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nWidget searchUserPanel(BuildContext context, ctr, list) {\n  TextStyle style = TextStyle(\n      fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,\n      color: Theme.of(context).colorScheme.outline);\n\n  return ListView.builder(\n    controller: ctr!.scrollController,\n    addAutomaticKeepAlives: false,\n    addRepaintBoundaries: false,\n    itemCount: list!.length,\n    itemBuilder: (context, index) {\n      var i = list![index];\n      String heroTag = Utils.makeHeroTag(i!.mid);\n      return InkWell(\n        onTap: () => Get.toNamed('/member?mid=${i.mid}',\n            arguments: {'heroTag': heroTag, 'face': i.upic}),\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),\n          child: Row(\n            children: [\n              Hero(\n                tag: heroTag,\n                child: NetworkImgLayer(\n                  width: 42,\n                  height: 42,\n                  src: i.upic,\n                  type: 'avatar',\n                ),\n              ),\n              const SizedBox(width: 10),\n              Column(\n                mainAxisSize: MainAxisSize.max,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  Row(\n                    children: [\n                      Text(\n                        i!.uname,\n                        style: const TextStyle(\n                          fontSize: 14,\n                        ),\n                      ),\n                      const SizedBox(width: 6),\n                      Image.asset(\n                        'assets/images/lv/lv${i!.level}.png',\n                        height: 11,\n                      ),\n                    ],\n                  ),\n                  Row(\n                    children: [\n                      Text('粉丝：${i.fans} ', style: style),\n                      Text(' 视频：${i.videos}', style: style)\n                    ],\n                  ),\n                  if (i.officialVerify['desc'] != '')\n                    Text(\n                      i.officialVerify['desc'],\n                      style: style,\n                    ),\n                ],\n              )\n            ],\n          ),\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/pages/search_panel/widgets/video_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/video_card_h.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/pages/search/widgets/search_text.dart';\nimport 'package:pilipala/pages/search_panel/index.dart';\n\nclass SearchVideoPanel extends StatelessWidget {\n  SearchVideoPanel({\n    this.ctr,\n    this.list,\n    Key? key,\n  }) : super(key: key);\n\n  final SearchPanelController? ctr;\n  final List? list;\n\n  final VideoPanelController controller = Get.put(VideoPanelController());\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      alignment: Alignment.topCenter,\n      children: [\n        Padding(\n          padding: const EdgeInsets.only(top: 36),\n          child: list!.isNotEmpty\n              ? ListView.builder(\n                  controller: ctr!.scrollController,\n                  addAutomaticKeepAlives: false,\n                  addRepaintBoundaries: false,\n                  itemCount: list!.length,\n                  itemBuilder: (context, index) {\n                    var i = list![index];\n                    return Padding(\n                      padding: index == 0\n                          ? const EdgeInsets.only(top: 2)\n                          : EdgeInsets.zero,\n                      child: VideoCardH(\n                        videoItem: i,\n                        showPubdate: true,\n                        source: 'search',\n                      ),\n                    );\n                  },\n                )\n              : CustomScrollView(\n                  slivers: [\n                    HttpError(\n                      errMsg: '没有数据',\n                      isShowBtn: false,\n                      fn: () => {},\n                    )\n                  ],\n                ),\n        ),\n        // 分类筛选\n        Container(\n          width: double.infinity,\n          height: 36,\n          padding: const EdgeInsets.only(left: 8, top: 0, right: 12),\n          // decoration: BoxDecoration(\n          //   border: Border(\n          //     bottom: BorderSide(\n          //       color: Theme.of(context).colorScheme.primary.withOpacity(0.1),\n          //     ),\n          //   ),\n          // ),\n          child: Row(\n            children: [\n              Expanded(\n                child: SingleChildScrollView(\n                  scrollDirection: Axis.horizontal,\n                  child: Obx(\n                    () => Wrap(\n                      // spacing: ,\n                      children: [\n                        for (var i in controller.filterList) ...[\n                          CustomFilterChip(\n                            label: i['label'],\n                            type: i['type'],\n                            selectedType: controller.selectedType.value,\n                            callFn: (bool selected) async {\n                              controller.selectedType.value = i['type'];\n                              ctr!.order.value =\n                                  i['type'].toString().split('.').last;\n                              SmartDialog.showLoading(msg: 'loading');\n                              await ctr!.onRefresh();\n                              SmartDialog.dismiss();\n                            },\n                          ),\n                        ]\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n              const VerticalDivider(indent: 7, endIndent: 8),\n              const SizedBox(width: 3),\n              SizedBox(\n                width: 32,\n                height: 32,\n                child: IconButton(\n                  style: ButtonStyle(\n                    padding: MaterialStateProperty.all(EdgeInsets.zero),\n                  ),\n                  onPressed: () => controller.onShowFilterSheet(ctr),\n                  icon: Icon(\n                    Icons.filter_list_outlined,\n                    size: 18,\n                    color: Theme.of(context).colorScheme.primary,\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ), // 放置在ListView.builder()上方的组件\n      ],\n    );\n  }\n}\n\nclass CustomFilterChip extends StatelessWidget {\n  const CustomFilterChip({\n    this.label,\n    this.type,\n    this.selectedType,\n    this.callFn,\n    Key? key,\n  }) : super(key: key);\n\n  final String? label;\n  final ArchiveFilterType? type;\n  final ArchiveFilterType? selectedType;\n  final Function? callFn;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 34,\n      child: FilterChip(\n        padding: const EdgeInsets.only(left: 11, right: 11),\n        labelPadding: EdgeInsets.zero,\n        label: Text(\n          label!,\n          style: const TextStyle(fontSize: 13),\n        ),\n        labelStyle: TextStyle(\n            color: type == selectedType\n                ? Theme.of(context).colorScheme.primary\n                : Theme.of(context).colorScheme.outline),\n        selected: type == selectedType,\n        showCheckmark: false,\n        shape: ContinuousRectangleBorder(\n          borderRadius: BorderRadius.circular(12),\n        ),\n        selectedColor: Colors.transparent,\n        // backgroundColor:\n        //     Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),\n        backgroundColor: Colors.transparent,\n        side: BorderSide.none,\n        onSelected: (bool selected) => callFn!(selected),\n      ),\n    );\n  }\n}\n\nclass VideoPanelController extends GetxController {\n  RxList<Map> filterList = [{}].obs;\n  Rx<ArchiveFilterType> selectedType = ArchiveFilterType.values.first.obs;\n  List<Map<String, dynamic>> timeFiltersList = [\n    {'label': '全部时长', 'value': 0},\n    {'label': '0-10分钟', 'value': 1},\n    {'label': '10-30分钟', 'value': 2},\n    {'label': '30-60分钟', 'value': 3},\n    {'label': '60分钟+', 'value': 4},\n  ];\n  List<Map<String, dynamic>> partFiltersList = [\n    {'label': '全部', 'value': -1},\n    {'label': '动画', 'value': 1},\n    {'label': '番剧', 'value': 13},\n    {'label': '国创', 'value': 167},\n    {'label': '音乐', 'value': 3},\n    {'label': '舞蹈', 'value': 129},\n    {'label': '游戏', 'value': 4},\n    {'label': '知识', 'value': 36},\n    {'label': '科技', 'value': 188},\n    {'label': '运动', 'value': 234},\n    {'label': '汽车', 'value': 223},\n    {'label': '生活', 'value': 160},\n    {'label': '美食', 'value': 211},\n    {'label': '动物', 'value': 217},\n    {'label': '鬼畜', 'value': 119},\n    {'label': '时尚', 'value': 155},\n    {'label': '资讯', 'value': 202},\n    {'label': '娱乐', 'value': 5},\n    {'label': '影视', 'value': 181},\n    {'label': '记录', 'value': 177},\n    {'label': '电影', 'value': 23},\n    {'label': '电视', 'value': 11},\n  ];\n\n  RxInt currentTimeFilterval = 0.obs;\n  RxInt currentPartFilterval = (-1).obs;\n\n  @override\n  void onInit() {\n    List<Map<String, dynamic>> list = ArchiveFilterType.values\n        .map((type) => {\n              'label': type.description,\n              'type': type,\n            })\n        .toList();\n    filterList.value = list;\n    super.onInit();\n  }\n\n  onShowFilterDialog(searchPanelCtr) {\n    SmartDialog.show(\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        TextStyle textStyle = Theme.of(context).textTheme.titleMedium!;\n        return AlertDialog(\n          title: const Text('时长筛选'),\n          contentPadding: const EdgeInsets.fromLTRB(0, 15, 0, 20),\n          content: StatefulBuilder(builder: (context, StateSetter setState) {\n            return Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                for (var i in timeFiltersList) ...[\n                  RadioListTile(\n                    value: i['value'],\n                    autofocus: true,\n                    title: Text(i['label'], style: textStyle),\n                    groupValue: currentTimeFilterval.value,\n                    onChanged: (value) async {\n                      currentTimeFilterval.value = value!;\n                      setState(() {});\n                      SmartDialog.dismiss();\n                      SmartDialog.showToast(\"「${i['label']}」的筛选结果\");\n                      SearchPanelController ctr =\n                          Get.find<SearchPanelController>(\n                              tag: 'video${searchPanelCtr.keyword!}');\n                      ctr.duration.value = i['value'];\n                      SmartDialog.showLoading(msg: 'loading');\n                      await ctr.onRefresh();\n                      SmartDialog.dismiss();\n                    },\n                  ),\n                ],\n              ],\n            );\n          }),\n        );\n      },\n    );\n  }\n\n  onShowFilterSheet(searchPanelCtr) {\n    showModalBottomSheet(\n      context: Get.context!,\n      isScrollControlled: true,\n      builder: (context) {\n        return StatefulBuilder(\n          builder: (context, StateSetter setState) {\n            return Padding(\n              padding: EdgeInsets.only(\n                  top: 12, bottom: MediaQuery.of(context).padding.bottom + 20),\n              child: Wrap(\n                children: [\n                  Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      const ListTile(\n                        title: Text('内容时长'),\n                      ),\n                      Padding(\n                        padding: const EdgeInsets.only(\n                          left: 14,\n                          right: 14,\n                          bottom: 14,\n                        ),\n                        child: Wrap(\n                          spacing: 10,\n                          runSpacing: 10,\n                          direction: Axis.horizontal,\n                          textDirection: TextDirection.ltr,\n                          children: [\n                            for (var i in timeFiltersList)\n                              Obx(\n                                () => SearchText(\n                                  searchText: i['label'],\n                                  searchTextIdx: i['value'],\n                                  isSelect:\n                                      currentTimeFilterval.value == i['value'],\n                                  onSelect: (value) async {\n                                    currentTimeFilterval.value = i['value'];\n                                    setState(() {});\n                                    SmartDialog.showToast(\n                                        \"「${i['label']}」的筛选结果\");\n                                    SearchPanelController ctr = Get.find<\n                                            SearchPanelController>(\n                                        tag: 'video${searchPanelCtr.keyword!}');\n                                    ctr.duration.value = i['value'];\n                                    Get.back();\n                                    SmartDialog.showLoading(msg: '获取中');\n                                    await ctr.onRefresh();\n                                    SmartDialog.dismiss();\n                                  },\n                                  onLongSelect: (value) => {},\n                                ),\n                              )\n                          ],\n                        ),\n                      ),\n                      const ListTile(\n                        title: Text('内容分区'),\n                      ),\n                      Padding(\n                        padding: const EdgeInsets.only(left: 14, right: 14),\n                        child: Wrap(\n                          spacing: 10,\n                          runSpacing: 10,\n                          direction: Axis.horizontal,\n                          textDirection: TextDirection.ltr,\n                          children: [\n                            for (var i in partFiltersList)\n                              SearchText(\n                                searchText: i['label'],\n                                searchTextIdx: i['value'],\n                                isSelect:\n                                    currentPartFilterval.value == i['value'],\n                                onSelect: (value) async {\n                                  currentPartFilterval.value = i['value'];\n                                  setState(() {});\n                                  SmartDialog.showToast(\"「${i['label']}」的筛选结果\");\n                                  SearchPanelController ctr = Get.find<\n                                          SearchPanelController>(\n                                      tag: 'video${searchPanelCtr.keyword!}');\n                                  ctr.tids.value = i['value'];\n                                  Get.back();\n                                  SmartDialog.showLoading(msg: '获取中');\n                                  await ctr.onRefresh();\n                                  SmartDialog.dismiss();\n                                },\n                                onLongSelect: (value) => {},\n                              )\n                          ],\n                        ),\n                      )\n                    ],\n                  ),\n                ],\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/search_result/controller.dart",
    "content": "import 'package:get/get.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/common/search_type.dart';\n\nclass SearchResultController extends GetxController {\n  String? keyword;\n  int tabIndex = 0;\n  RxList searchTabs = [].obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    if (Get.parameters.keys.isNotEmpty) {\n      keyword = Get.parameters['keyword'];\n    }\n    searchTabs.value = SearchType.values\n        .map((type) => {'label': type.label, 'id': type.type})\n        .toList();\n    querySearchCount();\n  }\n\n  Future querySearchCount() async {\n    var result = await SearchHttp.searchCount(keyword: keyword!);\n    if (result['status']) {\n      for (var i in searchTabs) {\n        final count = result['data'].topTList[i['id']];\n        i['count'] = count > 99 ? '99+' : count.toString();\n      }\n      searchTabs.refresh();\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/pages/search_result/index.dart",
    "content": "library searchresult;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/search_result/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/pages/search_panel/index.dart';\nimport 'controller.dart';\n\nclass SearchResultPage extends StatefulWidget {\n  const SearchResultPage({super.key});\n\n  @override\n  State<SearchResultPage> createState() => _SearchResultPageState();\n}\n\nclass _SearchResultPageState extends State<SearchResultPage>\n    with TickerProviderStateMixin {\n  late SearchResultController _searchResultController;\n  late TabController? _tabController;\n\n  @override\n  void initState() {\n    super.initState();\n    _searchResultController = Get.put(SearchResultController(),\n        tag: DateTime.now().millisecondsSinceEpoch.toString());\n\n    _tabController = TabController(\n      vsync: this,\n      length: SearchType.values.length,\n      initialIndex: _searchResultController.tabIndex,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        shape: Border(\n          bottom: BorderSide(\n            color: Theme.of(context).dividerColor.withOpacity(0.08),\n            width: 1,\n          ),\n        ),\n        titleSpacing: 0,\n        centerTitle: false,\n        title: GestureDetector(\n          onTap: () => Get.back(),\n          child: SizedBox(\n            width: double.infinity,\n            child: Text(\n              '${_searchResultController.keyword}',\n              style: Theme.of(context).textTheme.titleMedium,\n            ),\n          ),\n        ),\n      ),\n      body: Column(\n        children: [\n          const SizedBox(height: 4),\n          Container(\n            width: double.infinity,\n            padding: const EdgeInsets.only(left: 8),\n            color: Theme.of(context).colorScheme.surface,\n            child: Theme(\n              data: ThemeData(\n                splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明\n                highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明\n              ),\n              child: Obx(\n                () => (TabBar(\n                  controller: _tabController,\n                  tabs: [\n                    for (var i in _searchResultController.searchTabs)\n                      Tab(text: \"${i['label']} ${i['count'] ?? ''}\")\n                  ],\n                  isScrollable: true,\n                  indicatorWeight: 0,\n                  indicatorPadding:\n                      const EdgeInsets.symmetric(horizontal: 3, vertical: 8),\n                  indicator: BoxDecoration(\n                    color: Theme.of(context).colorScheme.secondaryContainer,\n                    borderRadius: const BorderRadius.all(Radius.circular(20)),\n                  ),\n                  indicatorSize: TabBarIndicatorSize.tab,\n                  labelColor:\n                      Theme.of(context).colorScheme.onSecondaryContainer,\n                  labelStyle: const TextStyle(fontSize: 13),\n                  dividerColor: Colors.transparent,\n                  unselectedLabelColor: Theme.of(context).colorScheme.outline,\n                  tabAlignment: TabAlignment.start,\n                  onTap: (index) {\n                    if (index == _searchResultController.tabIndex) {\n                      Get.find<SearchPanelController>(\n                              tag: SearchType.values[index].type +\n                                  _searchResultController.keyword!)\n                          .animateToTop();\n                    }\n\n                    _searchResultController.tabIndex = index;\n                  },\n                )),\n              ),\n            ),\n          ),\n          Expanded(\n            child: TabBarView(\n              controller: _tabController,\n              children: [\n                for (var i in SearchType.values) ...{\n                  SearchPanel(\n                    keyword: _searchResultController.keyword,\n                    searchType: i,\n                    tag: DateTime.now().millisecondsSinceEpoch.toString(),\n                  )\n                }\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/init.dart';\nimport 'package:pilipala/models/common/theme_type.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/login.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport '../../models/common/dynamic_badge_mode.dart';\nimport '../../models/common/nav_bar_config.dart';\nimport '../main/index.dart';\nimport 'widgets/select_dialog.dart';\n\nclass SettingController extends GetxController {\n  Box userInfoCache = GStrorage.userInfo;\n  Box setting = GStrorage.setting;\n  Box localCache = GStrorage.localCache;\n\n  RxBool userLogin = false.obs;\n  RxBool feedBackEnable = false.obs;\n  RxDouble toastOpacity = (1.0).obs;\n  RxInt picQuality = 10.obs;\n  Rx<ThemeType> themeType = ThemeType.system.obs;\n  var userInfo;\n  Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;\n  RxInt defaultHomePage = 0.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n    userLogin.value = userInfo != null;\n    feedBackEnable.value =\n        setting.get(SettingBoxKey.feedBackEnable, defaultValue: false);\n    toastOpacity.value =\n        setting.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0);\n    picQuality.value =\n        setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);\n    themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,\n        defaultValue: ThemeType.system.code)];\n    dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(\n        SettingBoxKey.dynamicBadgeMode,\n        defaultValue: DynamicBadgeMode.number.code)];\n    defaultHomePage.value =\n        setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0);\n  }\n\n  loginOut() async {\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: const Text('确认要退出登录吗'),\n          actions: [\n            TextButton(\n              onPressed: () => SmartDialog.dismiss(),\n              child: const Text('点错了'),\n            ),\n            TextButton(\n              onPressed: () async {\n                // 清空cookie\n                await Request.cookieManager.cookieJar.deleteAll();\n                Request.dio.options.headers['cookie'] = '';\n\n                // 清空本地存储的用户标识\n                userInfoCache.put('userInfoCache', null);\n                localCache\n                    .put(LocalCacheKey.accessKey, {'mid': -1, 'value': ''});\n\n                await LoginUtils.refreshLoginStatus(false);\n                SmartDialog.dismiss().then((value) => Get.back());\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  // 开启关闭震动反馈\n  onOpenFeedBack() {\n    feedBack();\n    feedBackEnable.value = !feedBackEnable.value;\n    setting.put(SettingBoxKey.feedBackEnable, feedBackEnable.value);\n  }\n\n  // 设置动态未读标记\n  setDynamicBadgeMode(BuildContext context) async {\n    DynamicBadgeMode? result = await showDialog(\n      context: context,\n      builder: (context) {\n        return SelectDialog<DynamicBadgeMode>(\n          title: '动态未读标记',\n          value: dynamicBadgeType.value,\n          values: DynamicBadgeMode.values.map((e) {\n            return {'title': e.description, 'value': e};\n          }).toList(),\n        );\n      },\n    );\n    if (result != null) {\n      dynamicBadgeType.value = result;\n      setting.put(SettingBoxKey.dynamicBadgeMode, result.code);\n      MainController mainController = Get.put(MainController());\n      mainController.dynamicBadgeType.value =\n          DynamicBadgeMode.values[result.code];\n      if (mainController.dynamicBadgeType.value != DynamicBadgeMode.hidden) {\n        mainController.getUnreadDynamic();\n      }\n      SmartDialog.showToast('设置成功');\n    }\n  }\n\n  // 设置默认启动页\n  seteDefaultHomePage(BuildContext context) async {\n    int? result = await showDialog(\n      context: context,\n      builder: (context) {\n        return SelectDialog<int>(\n            title: '首页启动页',\n            value: defaultHomePage.value,\n            values: defaultNavigationBars.map((e) {\n              return {'title': e['label'], 'value': e['id']};\n            }).toList());\n      },\n    );\n    if (result != null) {\n      defaultHomePage.value = result;\n      setting.put(SettingBoxKey.defaultHomePage, result);\n      SmartDialog.showToast('设置成功，重启生效');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/extra_setting.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/dynamics_type.dart';\nimport 'package:pilipala/models/common/reply_sort_type.dart';\nimport 'package:pilipala/pages/setting/widgets/select_dialog.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nimport '../home/index.dart';\nimport 'widgets/switch_item.dart';\n\nclass ExtraSetting extends StatefulWidget {\n  const ExtraSetting({super.key});\n\n  @override\n  State<ExtraSetting> createState() => _ExtraSettingState();\n}\n\nclass _ExtraSettingState extends State<ExtraSetting> {\n  Box setting = GStrorage.setting;\n  static Box localCache = GStrorage.localCache;\n  late dynamic defaultReplySort;\n  late dynamic defaultDynamicType;\n  late dynamic enableSystemProxy;\n  late String defaultSystemProxyHost;\n  late String defaultSystemProxyPort;\n  bool userLogin = false;\n\n  @override\n  void initState() {\n    super.initState();\n    // 默认优先显示最新评论\n    defaultReplySort =\n        setting.get(SettingBoxKey.replySortType, defaultValue: 0);\n    if (defaultReplySort == 2) {\n      setting.put(SettingBoxKey.replySortType, 0);\n      defaultReplySort = 0;\n    }\n    // 优先展示全部动态 all\n    defaultDynamicType =\n        setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);\n    enableSystemProxy =\n        setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);\n    defaultSystemProxyHost =\n        localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');\n    defaultSystemProxyPort =\n        localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');\n  }\n\n  // 设置代理\n  void twoFADialog() {\n    var systemProxyHost = '';\n    var systemProxyPort = '';\n\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('设置代理'),\n          content: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              const SizedBox(height: 6),\n              TextField(\n                decoration: InputDecoration(\n                  isDense: true,\n                  labelText: defaultSystemProxyHost != ''\n                      ? defaultSystemProxyHost\n                      : '请输入Host，使用 . 分割',\n                  border: OutlineInputBorder(\n                    borderRadius: BorderRadius.circular(6.0),\n                  ),\n                  hintText: defaultSystemProxyHost,\n                ),\n                onChanged: (e) {\n                  systemProxyHost = e;\n                },\n              ),\n              const SizedBox(height: 10),\n              TextField(\n                keyboardType: TextInputType.number,\n                decoration: InputDecoration(\n                  isDense: true,\n                  labelText: defaultSystemProxyPort != ''\n                      ? defaultSystemProxyPort\n                      : '请输入Port',\n                  border: OutlineInputBorder(\n                    borderRadius: BorderRadius.circular(6.0),\n                  ),\n                  hintText: defaultSystemProxyPort,\n                ),\n                onChanged: (e) {\n                  systemProxyPort = e;\n                },\n              ),\n            ],\n          ),\n          actions: [\n            TextButton(\n              onPressed: () async {\n                SmartDialog.dismiss();\n              },\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                localCache.put(LocalCacheKey.systemProxyHost, systemProxyHost);\n                localCache.put(LocalCacheKey.systemProxyPort, systemProxyPort);\n                SmartDialog.dismiss();\n                // Request.dio;\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;\n    TextStyle subTitleStyle = Theme.of(context)\n        .textTheme\n        .labelMedium!\n        .copyWith(color: Theme.of(context).colorScheme.outline);\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          '其他设置',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: ListView(\n        children: [\n          const SetSwitchItem(\n            title: '大家都在搜',\n            subTitle: '是否展示「大家都在搜」',\n            setKey: SettingBoxKey.enableHotKey,\n            defaultVal: true,\n          ),\n          SetSwitchItem(\n            title: '搜索默认词',\n            subTitle: '是否展示搜索框默认词',\n            setKey: SettingBoxKey.enableSearchWord,\n            defaultVal: true,\n            callFn: (val) {\n              Get.find<HomeController>().defaultSearch.value = '';\n            },\n          ),\n          const SetSwitchItem(\n            title: '快速收藏',\n            subTitle: '点按收藏至默认，长按选择文件夹',\n            setKey: SettingBoxKey.enableQuickFav,\n            defaultVal: false,\n          ),\n          const SetSwitchItem(\n            title: '评论区搜索关键词',\n            subTitle: '展示评论区搜索关键词',\n            setKey: SettingBoxKey.enableWordRe,\n            defaultVal: false,\n          ),\n          const SetSwitchItem(\n            title: '启用ai总结',\n            subTitle: '视频详情页开启ai总结',\n            setKey: SettingBoxKey.enableAi,\n            defaultVal: true,\n          ),\n          const SetSwitchItem(\n            title: '相关视频推荐',\n            subTitle: '视频详情页推荐相关视频',\n            setKey: SettingBoxKey.enableRelatedVideo,\n            defaultVal: true,\n          ),\n          ListTile(\n            dense: false,\n            title: Text('评论展示', style: titleStyle),\n            subtitle: Text(\n              '当前优先展示「${ReplySortType.values[defaultReplySort].titles}」',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '评论展示',\n                      value: defaultReplySort,\n                      values: ReplySortType.values.map((e) {\n                        return {'title': e.titles, 'value': e.index};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultReplySort = result;\n                setting.put(SettingBoxKey.replySortType, result);\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('动态展示', style: titleStyle),\n            subtitle: Text(\n              '当前优先展示「${DynamicsType.values[defaultDynamicType].labels}」',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '动态展示',\n                      value: defaultDynamicType,\n                      values: DynamicsType.values.map((e) {\n                        return {'title': e.labels, 'value': e.index};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultDynamicType = result;\n                setting.put(SettingBoxKey.defaultDynamicType, result);\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            enableFeedback: true,\n            onTap: () => twoFADialog(),\n            title: Text('设置代理', style: titleStyle),\n            subtitle: Text('设置代理 host:port', style: subTitleStyle),\n            trailing: Transform.scale(\n              alignment: Alignment.centerRight,\n              scale: 0.8,\n              child: Switch(\n                thumbIcon: MaterialStateProperty.resolveWith<Icon?>(\n                    (Set<MaterialState> states) {\n                  if (states.isNotEmpty &&\n                      states.first == MaterialState.selected) {\n                    return const Icon(Icons.done);\n                  }\n                  return null; // All other states will use the default thumbIcon.\n                }),\n                value: enableSystemProxy,\n                onChanged: (val) {\n                  setting.put(\n                      SettingBoxKey.enableSystemProxy, !enableSystemProxy);\n                  setState(() {\n                    enableSystemProxy = !enableSystemProxy;\n                  });\n                },\n              ),\n            ),\n          ),\n          const SetSwitchItem(\n            title: '检查更新',\n            subTitle: '每次启动时检查是否需要更新',\n            setKey: SettingBoxKey.autoUpdate,\n            defaultVal: false,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/index.dart",
    "content": "library setting;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/setting/pages/action_menu_set.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/action_type.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\nimport '../../../utils/storage.dart';\n\nclass ActionMenuSetPage extends StatefulWidget {\n  const ActionMenuSetPage({super.key});\n\n  @override\n  State<ActionMenuSetPage> createState() => _ActionMenuSetPageState();\n}\n\nclass _ActionMenuSetPageState extends State<ActionMenuSetPage> {\n  Box setting = GStrorage.setting;\n  late List<String> actionTypeSort;\n  late List<Map> allLabels;\n\n  @override\n  void initState() {\n    super.initState();\n    actionTypeSort = setting.get(SettingBoxKey.actionTypeSort,\n        defaultValue: ['like', 'coin', 'collect', 'watchLater', 'share']);\n    allLabels = actionMenuConfig;\n    allLabels.sort((a, b) {\n      int indexA = actionTypeSort.indexOf((a['value'] as ActionType).value);\n      int indexB = actionTypeSort.indexOf((b['value'] as ActionType).value);\n      if (indexA == -1) indexA = actionTypeSort.length;\n      if (indexB == -1) indexB = actionTypeSort.length;\n      return indexA.compareTo(indexB);\n    });\n  }\n\n  void saveEdit() {\n    List<String> sortedTabbar = allLabels\n        .where((i) => actionTypeSort.contains((i['value'] as ActionType).value))\n        .map<String>((i) => (i['value'] as ActionType).value)\n        .toList();\n    setting.put(SettingBoxKey.actionTypeSort, sortedTabbar);\n    GlobalDataCache().actionTypeSort = sortedTabbar;\n    SmartDialog.showToast('操作成功');\n  }\n\n  void onReorder(int oldIndex, int newIndex) {\n    setState(() {\n      if (newIndex > oldIndex) {\n        newIndex -= 1;\n      }\n      final tabsItem = allLabels.removeAt(oldIndex);\n      allLabels.insert(newIndex, tabsItem);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final listTiles = [\n      for (int i = 0; i < allLabels.length; i++) ...[\n        CheckboxListTile(\n          key: Key((allLabels[i]['value'] as ActionType).value),\n          value: actionTypeSort\n              .contains((allLabels[i]['value'] as ActionType).value),\n          onChanged: (bool? newValue) {\n            String actionTypeId = (allLabels[i]['value'] as ActionType).value;\n            if (!newValue!) {\n              actionTypeSort.remove(actionTypeId);\n            } else {\n              actionTypeSort.add(actionTypeId);\n            }\n            setState(() {});\n          },\n          title: Row(\n            children: [\n              allLabels[i]['icon'],\n              const SizedBox(width: 8),\n              Text(allLabels[i]['label']),\n            ],\n          ),\n          secondary: const Icon(Icons.drag_indicator_rounded),\n        )\n      ]\n    ];\n\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('视频操作菜单'),\n        actions: [\n          TextButton(onPressed: () => saveEdit(), child: const Text('保存')),\n          const SizedBox(width: 12)\n        ],\n      ),\n      body: ReorderableListView(\n        onReorder: onReorder,\n        physics: const NeverScrollableScrollPhysics(),\n        footer: SizedBox(\n          height: MediaQuery.of(context).padding.bottom + 30,\n        ),\n        children: listTiles,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/pages/color_select.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/color_type.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass ColorSelectPage extends StatefulWidget {\n  const ColorSelectPage({super.key});\n\n  @override\n  State<ColorSelectPage> createState() => _ColorSelectPageState();\n}\n\nclass Item {\n  Item({\n    required this.expandedValue,\n    required this.headerValue,\n    this.isExpanded = false,\n  });\n\n  String expandedValue;\n  String headerValue;\n  bool isExpanded;\n}\n\nList<Item> generateItems(int count) {\n  return List<Item>.generate(count, (int index) {\n    return Item(\n      headerValue: 'Panel $index',\n      expandedValue: 'This is item number $index',\n    );\n  });\n}\n\nclass _ColorSelectPageState extends State<ColorSelectPage> {\n  final ColorSelectController ctr = Get.put(ColorSelectController());\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        title: const Text('选择应用主题'),\n      ),\n      body: ListView(\n        children: [\n          Obx(\n            () => RadioListTile(\n              value: 0,\n              title: const Text('动态取色'),\n              groupValue: ctr.type.value,\n              onChanged: (dynamic val) async {\n                ctr.type.value = 0;\n                ctr.setting.put(SettingBoxKey.dynamicColor, true);\n              },\n            ),\n          ),\n          Obx(\n            () => RadioListTile(\n              value: 1,\n              title: const Text('指定颜色'),\n              groupValue: ctr.type.value,\n              onChanged: (dynamic val) async {\n                ctr.type.value = 1;\n                ctr.setting.put(SettingBoxKey.dynamicColor, false);\n              },\n            ),\n          ),\n          Obx(\n            () {\n              int type = ctr.type.value;\n              return AnimatedOpacity(\n                opacity: type == 1 ? 1 : 0,\n                duration: const Duration(milliseconds: 200),\n                child: Padding(\n                  padding: const EdgeInsets.only(top: 12, left: 12, right: 12),\n                  child: Wrap(\n                    alignment: WrapAlignment.center,\n                    spacing: 22,\n                    runSpacing: 18,\n                    children: [\n                      ...ctr.colorThemes.map(\n                        (e) {\n                          final index = ctr.colorThemes.indexOf(e);\n                          return GestureDetector(\n                            onTap: () {\n                              ctr.currentColor.value = index;\n                              ctr.setting.put(SettingBoxKey.customColor, index);\n                              Get.forceAppUpdate();\n                            },\n                            child: Column(\n                              children: [\n                                Container(\n                                  width: 46,\n                                  height: 46,\n                                  decoration: BoxDecoration(\n                                    color: e['color'].withOpacity(0.8),\n                                    borderRadius: BorderRadius.circular(50),\n                                    border: Border.all(\n                                      width: 2,\n                                      color: ctr.currentColor.value == index\n                                          ? Colors.black\n                                          : e['color'].withOpacity(0.8),\n                                    ),\n                                  ),\n                                  child: AnimatedOpacity(\n                                    opacity:\n                                        ctr.currentColor.value == index ? 1 : 0,\n                                    duration: const Duration(milliseconds: 200),\n                                    child: const Icon(\n                                      Icons.done,\n                                      color: Colors.black,\n                                      size: 20,\n                                    ),\n                                  ),\n                                ),\n                                const SizedBox(height: 3),\n                                Text(\n                                  e['label'],\n                                  style: TextStyle(\n                                    fontSize: 12,\n                                    color: ctr.currentColor.value != index\n                                        ? Theme.of(context).colorScheme.outline\n                                        : null,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          );\n                        },\n                      )\n                    ],\n                  ),\n                ),\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass ColorSelectController extends GetxController {\n  Box setting = GStrorage.setting;\n  RxBool dynamicColor = true.obs;\n  RxInt type = 0.obs;\n  late final List<Map<String, dynamic>> colorThemes;\n  RxInt currentColor = 0.obs;\n\n  @override\n  void onInit() {\n    colorThemes = colorThemeTypes;\n    // 默认使用动态取色\n    dynamicColor.value =\n        setting.get(SettingBoxKey.dynamicColor, defaultValue: true);\n    type.value = dynamicColor.value ? 0 : 1;\n    currentColor.value =\n        setting.get(SettingBoxKey.customColor, defaultValue: 0);\n    super.onInit();\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/pages/display_mode.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/scheduler.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_displaymode/flutter_displaymode.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass SetDiaplayMode extends StatefulWidget {\n  const SetDiaplayMode({super.key});\n\n  @override\n  State<SetDiaplayMode> createState() => _SetDiaplayModeState();\n}\n\nclass _SetDiaplayModeState extends State<SetDiaplayMode> {\n  List<DisplayMode> modes = <DisplayMode>[];\n  DisplayMode? active;\n  DisplayMode? preferred;\n  Box setting = GStrorage.setting;\n\n  final ValueNotifier<int> page = ValueNotifier<int>(0);\n  late final PageController controller = PageController()\n    ..addListener(() {\n      page.value = controller.page!.round();\n    });\n  @override\n  void initState() {\n    super.initState();\n    init();\n    SchedulerBinding.instance.addPostFrameCallback((_) {\n      fetchAll();\n    });\n  }\n\n  // 获取所有的mode\n  Future<void> fetchAll() async {\n    preferred = await FlutterDisplayMode.preferred;\n    active = await FlutterDisplayMode.active;\n    await setting.put(SettingBoxKey.displayMode, preferred.toString());\n    setState(() {});\n  }\n\n  // 初始化mode/手动设置\n  Future<void> init() async {\n    try {\n      modes = await FlutterDisplayMode.supported;\n    } on PlatformException catch (e) {\n      print(e);\n    }\n    var res = await getDisplayModeType(modes);\n\n    preferred = modes.toList().firstWhere((el) => el == res);\n    FlutterDisplayMode.setPreferredMode(preferred!);\n  }\n\n  Future<DisplayMode> getDisplayModeType(modes) async {\n    var value = setting.get(SettingBoxKey.displayMode);\n    DisplayMode f = DisplayMode.auto;\n    if (value != null) {\n      f = modes.firstWhere((e) => e.toString() == value);\n    }\n    return f;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('屏幕帧率设置')),\n      body: SafeArea(\n        top: false,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: <Widget>[\n            if (modes.isEmpty) const Text('Nothing here'),\n            Padding(\n              padding: const EdgeInsets.only(left: 25, top: 10, bottom: 5),\n              child: Text(\n                '没有生效？重启app试试',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            Expanded(\n              child: ListView.builder(\n                itemCount: modes.length,\n                itemBuilder: (_, int i) {\n                  final DisplayMode mode = modes[i];\n                  return RadioListTile<DisplayMode>(\n                    value: mode,\n                    title: mode == DisplayMode.auto\n                        ? const Text('自动')\n                        : Text('$mode${mode == active ? \"  [系统]\" : \"\"}'),\n                    groupValue: preferred,\n                    onChanged: (DisplayMode? newMode) async {\n                      await FlutterDisplayMode.setPreferredMode(newMode!);\n                      await Future<dynamic>.delayed(\n                        const Duration(milliseconds: 100),\n                      );\n                      await fetchAll();\n                    },\n                  );\n                },\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/pages/font_size_select.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass FontSizeSelectPage extends StatefulWidget {\n  const FontSizeSelectPage({super.key});\n\n  @override\n  State<FontSizeSelectPage> createState() => _FontSizeSelectPageState();\n}\n\nclass _FontSizeSelectPageState extends State<FontSizeSelectPage> {\n  Box setting = GStrorage.setting;\n  List<double> list = [0.9, 0.95, 1.0, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3];\n  late double minsize;\n  late double maxSize;\n  late double currentSize;\n\n  @override\n  void initState() {\n    super.initState();\n    minsize = list.first;\n    maxSize = list.last;\n    currentSize =\n        setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);\n  }\n\n  setFontSize() {\n    setting.put(SettingBoxKey.defaultTextScale, currentSize);\n    Get.forceAppUpdate();\n    Get.back();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        actions: [\n          TextButton(onPressed: () => setFontSize(), child: const Text('确定')),\n          const SizedBox(width: 12)\n        ],\n      ),\n      body: Column(\n        children: [\n          Expanded(\n            child: Center(\n              child: Text(\n                '当前字体大小:${currentSize == 1.0 ? '默认' : currentSize}',\n                style: TextStyle(fontSize: 14 * currentSize),\n              ),\n            ),\n          ),\n          Container(\n            width: double.infinity,\n            padding: EdgeInsets.only(\n              left: 20,\n              right: 20,\n              top: 20,\n              bottom: MediaQuery.of(context).padding.bottom + 20,\n            ),\n            decoration: BoxDecoration(\n              border: Border(\n                  top: BorderSide(\n                      color: Theme.of(context)\n                          .colorScheme\n                          .primary\n                          .withOpacity(0.3))),\n              color: Theme.of(context).colorScheme.surface,\n            ),\n            child: Row(\n              children: [\n                const Text('小'),\n                Expanded(\n                  child: Slider(\n                    min: minsize,\n                    value: currentSize,\n                    max: maxSize,\n                    divisions: list.length - 1,\n                    secondaryTrackValue: 1,\n                    onChanged: (double val) {\n                      currentSize = double.parse(val.toStringAsFixed(2));\n                      setState(() {});\n                    },\n                  ),\n                ),\n                const SizedBox(width: 5),\n                const Text(\n                  '大',\n                  style: TextStyle(fontSize: 20),\n                ),\n              ],\n            ),\n          )\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/pages/home_tabbar_set.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/tab_type.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass TabbarSetPage extends StatefulWidget {\n  const TabbarSetPage({super.key});\n\n  @override\n  State<TabbarSetPage> createState() => _TabbarSetPageState();\n}\n\nclass _TabbarSetPageState extends State<TabbarSetPage> {\n  Box settingStorage = GStrorage.setting;\n  late List defaultTabs;\n  late List<String> tabbarSort;\n\n  @override\n  void initState() {\n    super.initState();\n    defaultTabs = tabsConfig;\n    tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,\n        defaultValue: ['live', 'rcmd', 'hot', 'bangumi']);\n    // 对 tabData 进行排序\n    defaultTabs.sort((a, b) {\n      int indexA = tabbarSort.indexOf((a['type'] as TabType).id);\n      int indexB = tabbarSort.indexOf((b['type'] as TabType).id);\n\n      // 如果类型在 sortOrder 中不存在，则放在末尾\n      if (indexA == -1) indexA = tabbarSort.length;\n      if (indexB == -1) indexB = tabbarSort.length;\n\n      return indexA.compareTo(indexB);\n    });\n  }\n\n  void saveEdit() {\n    List<String> sortedTabbar = defaultTabs\n        .where((i) => tabbarSort.contains((i['type'] as TabType).id))\n        .map<String>((i) => (i['type'] as TabType).id)\n        .toList();\n    settingStorage.put(SettingBoxKey.tabbarSort, sortedTabbar);\n    SmartDialog.showToast('保存成功，下次启动时生效');\n  }\n\n  void onReorder(int oldIndex, int newIndex) {\n    setState(() {\n      if (newIndex > oldIndex) {\n        newIndex -= 1;\n      }\n      final tabsItem = defaultTabs.removeAt(oldIndex);\n      defaultTabs.insert(newIndex, tabsItem);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final listTiles = [\n      for (int i = 0; i < defaultTabs.length; i++) ...[\n        CheckboxListTile(\n          key: Key(defaultTabs[i]['label']),\n          value: tabbarSort.contains((defaultTabs[i]['type'] as TabType).id),\n          onChanged: (bool? newValue) {\n            String tabTypeId = (defaultTabs[i]['type'] as TabType).id;\n            if (!newValue!) {\n              tabbarSort.remove(tabTypeId);\n            } else {\n              tabbarSort.add(tabTypeId);\n            }\n            setState(() {});\n          },\n          title: Text(defaultTabs[i]['label']),\n          secondary: const Icon(Icons.drag_indicator_rounded),\n        )\n      ]\n    ];\n\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Tabbar编辑'),\n        actions: [\n          TextButton(onPressed: () => saveEdit(), child: const Text('保存')),\n          const SizedBox(width: 12)\n        ],\n      ),\n      body: ReorderableListView(\n        onReorder: onReorder,\n        physics: const NeverScrollableScrollPhysics(),\n        footer: SizedBox(\n          height: MediaQuery.of(context).padding.bottom + 30,\n        ),\n        children: listTiles,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/pages/logs.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport '../../../services/loggeer.dart';\n\nclass LogsPage extends StatefulWidget {\n  const LogsPage({super.key});\n\n  @override\n  State<LogsPage> createState() => _LogsPageState();\n}\n\nclass _LogsPageState extends State<LogsPage> {\n  late File logsPath;\n  late String fileContent;\n  List logsContent = [];\n\n  @override\n  void initState() {\n    getPath();\n    super.initState();\n  }\n\n  void getPath() async {\n    logsPath = await getLogsPath();\n    fileContent = await logsPath.readAsString();\n    logsContent = await parseLogs(fileContent);\n    setState(() {});\n  }\n\n  Future<List<Map<String, dynamic>>> parseLogs(String fileContent) async {\n    const String splitToken =\n        '======================================================================';\n    List contentList = fileContent.split(splitToken).map((item) {\n      return item\n          .replaceAll(\n              '============================== CATCHER 2 LOG ==============================',\n              'Pilipala错误日志 \\n ********************')\n          .replaceAll('DEVICE INFO', '设备信息')\n          .replaceAll('APP INFO', '应用信息')\n          .replaceAll('ERROR', '错误信息')\n          .replaceAll('STACK TRACE', '错误堆栈')\n          .replaceAll('#', 'Line');\n    }).toList();\n    List<Map<String, dynamic>> result = [];\n    for (String i in contentList) {\n      DateTime? date;\n      String body = i\n          .split(\"\\n\")\n          .map((l) {\n            if (l.startsWith(\"Crash occurred on\")) {\n              date = DateTime.tryParse(\n                l.split(\"Crash occurred on\")[1].trim().split('.')[0],\n              );\n              return \"\";\n            }\n            return l;\n          })\n          .where((dynamic l) => l.replaceAll(\"\\n\", \"\").trim().isNotEmpty)\n          .join(\"\\n\");\n      if (date != null || body != '') {\n        result.add({'date': date, 'body': body, 'expand': false});\n      }\n    }\n    return result.reversed.toList();\n  }\n\n  void copyLogs() async {\n    await Clipboard.setData(ClipboardData(text: fileContent));\n    if (context.mounted) {\n      ScaffoldMessenger.of(context).showSnackBar(\n        const SnackBar(content: Text('复制成功')),\n      );\n    }\n  }\n\n  void feedback() {\n    launchUrl(\n      Uri.parse('https://github.com/guozhigq/pilipala/issues'),\n      // 系统自带浏览器打开\n      mode: LaunchMode.externalApplication,\n    );\n  }\n\n  void clearLogsHandle() async {\n    if (await clearLogs()) {\n      if (context.mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          const SnackBar(content: Text('已清空')),\n        );\n        logsContent = [];\n        setState(() {});\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text('日志', style: Theme.of(context).textTheme.titleMedium),\n        actions: [\n          PopupMenuButton<String>(\n            onSelected: (String type) {\n              // 处理菜单项选择的逻辑\n              switch (type) {\n                case 'copy':\n                  copyLogs();\n                  break;\n                case 'feedback':\n                  feedback();\n                  break;\n                case 'clear':\n                  clearLogsHandle();\n                  break;\n                default:\n              }\n            },\n            itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[\n              const PopupMenuItem<String>(\n                value: 'copy',\n                child: Text('复制日志'),\n              ),\n              const PopupMenuItem<String>(\n                value: 'feedback',\n                child: Text('错误反馈'),\n              ),\n              const PopupMenuItem<String>(\n                value: 'clear',\n                child: Text('清空日志'),\n              ),\n            ],\n          ),\n          const SizedBox(width: 6),\n        ],\n      ),\n      body: logsContent.isNotEmpty\n          ? ListView.builder(\n              itemCount: logsContent.length,\n              itemBuilder: (context, index) {\n                final log = logsContent[index];\n                return Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Row(\n                      children: [\n                        Padding(\n                          padding: const EdgeInsets.all(8.0),\n                          child: Text(\n                            log['date'].toString(),\n                            style: Theme.of(context).textTheme.titleMedium,\n                          ),\n                        ),\n                        TextButton.icon(\n                          onPressed: () async {\n                            await Clipboard.setData(\n                              ClipboardData(text: log['body']),\n                            );\n                            if (context.mounted) {\n                              ScaffoldMessenger.of(context).showSnackBar(\n                                SnackBar(\n                                  content: Text(\n                                    '已将 ${log['date'].toString()} 复制至剪贴板',\n                                  ),\n                                ),\n                              );\n                            }\n                          },\n                          icon: const Icon(Icons.copy_outlined, size: 16),\n                          label: const Text('复制'),\n                        )\n                      ],\n                    ),\n                    Padding(\n                      padding: const EdgeInsets.all(8.0),\n                      child: Card(\n                        elevation: 1,\n                        clipBehavior: Clip.antiAliasWithSaveLayer,\n                        child: Padding(\n                          padding: const EdgeInsets.all(12.0),\n                          child: SelectableText(log['body']),\n                        ),\n                      ),\n                    ),\n                    const Divider(indent: 12, endIndent: 12),\n                  ],\n                );\n              },\n            )\n          : const CustomScrollView(\n              slivers: <Widget>[\n                NoData(),\n              ],\n            ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/pages/navigation_bar_set.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/tab_type.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nimport '../../../models/common/nav_bar_config.dart';\n\nclass NavigationBarSetPage extends StatefulWidget {\n  const NavigationBarSetPage({super.key});\n\n  @override\n  State<NavigationBarSetPage> createState() => _NavigationbarSetPageState();\n}\n\nclass _NavigationbarSetPageState extends State<NavigationBarSetPage> {\n  Box settingStorage = GStrorage.setting;\n  late List defaultNavTabs;\n  late List<int> navBarSort;\n\n  @override\n  void initState() {\n    super.initState();\n    defaultNavTabs = defaultNavigationBars;\n    navBarSort = settingStorage\n        .get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]);\n    // 对 tabData 进行排序\n    defaultNavTabs.sort((a, b) {\n      int indexA = navBarSort.indexOf(a['id']);\n      int indexB = navBarSort.indexOf(b['id']);\n\n      // 如果类型在 sortOrder 中不存在，则放在末尾\n      if (indexA == -1) indexA = navBarSort.length;\n      if (indexB == -1) indexB = navBarSort.length;\n\n      return indexA.compareTo(indexB);\n    });\n  }\n\n  void saveEdit() {\n    List<int> sortedTabbar = defaultNavTabs\n        .where((i) => navBarSort.contains(i['id']))\n        .map<int>((i) => i['id'])\n        .toList();\n    settingStorage.put(SettingBoxKey.navBarSort, sortedTabbar);\n    SmartDialog.showToast('保存成功，下次启动时生效');\n  }\n\n  void onReorder(int oldIndex, int newIndex) {\n    setState(() {\n      if (newIndex > oldIndex) {\n        newIndex -= 1;\n      }\n      final tabsItem = defaultNavTabs.removeAt(oldIndex);\n      defaultNavTabs.insert(newIndex, tabsItem);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final listTiles = [\n      for (int i = 0; i < defaultNavTabs.length; i++) ...[\n        CheckboxListTile(\n          key: Key(defaultNavTabs[i]['label']),\n          value: navBarSort.contains(defaultNavTabs[i]['id']),\n          onChanged: (bool? newValue) {\n            int tabTypeId = defaultNavTabs[i]['id'];\n            if (!newValue!) {\n              navBarSort.remove(tabTypeId);\n            } else {\n              navBarSort.add(tabTypeId);\n            }\n            setState(() {});\n          },\n          title: Text(defaultNavTabs[i]['label']),\n          secondary: const Icon(Icons.drag_indicator_rounded),\n        )\n      ]\n    ];\n\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Navbar编辑'),\n        actions: [\n          TextButton(onPressed: () => saveEdit(), child: const Text('保存')),\n          const SizedBox(width: 12)\n        ],\n      ),\n      body: ReorderableListView(\n        onReorder: onReorder,\n        physics: const NeverScrollableScrollPhysics(),\n        footer: SizedBox(\n          height: MediaQuery.of(context).padding.bottom + 30,\n        ),\n        children: listTiles,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/pages/play_gesture_set.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\n\nimport '../../../models/common/gesture_mode.dart';\nimport '../../../utils/storage.dart';\nimport '../widgets/select_dialog.dart';\nimport '../widgets/switch_item.dart';\n\nclass PlayGesturePage extends StatefulWidget {\n  const PlayGesturePage({super.key});\n\n  @override\n  State<PlayGesturePage> createState() => _PlayGesturePageState();\n}\n\nclass _PlayGesturePageState extends State<PlayGesturePage> {\n  Box setting = GStrorage.setting;\n  late int fullScreenGestureMode;\n\n  @override\n  void initState() {\n    super.initState();\n    fullScreenGestureMode = setting.get(SettingBoxKey.fullScreenGestureMode,\n        defaultValue: FullScreenGestureMode.values.last.index);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;\n    TextStyle subTitleStyle = Theme.of(context)\n        .textTheme\n        .labelMedium!\n        .copyWith(color: Theme.of(context).colorScheme.outline);\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          '手势设置',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: ListView(\n        children: [\n          ListTile(\n            dense: false,\n            title: Text('全屏手势', style: titleStyle),\n            subtitle: Text(\n              '通过手势快速进入全屏',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              String? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<String>(\n                      title: '全屏手势',\n                      value: FullScreenGestureMode\n                          .values[fullScreenGestureMode].values,\n                      values: FullScreenGestureMode.values.map((e) {\n                        return {'title': e.labels, 'value': e.values};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                GlobalDataCache().fullScreenGestureMode = FullScreenGestureMode\n                    .values\n                    .firstWhere((element) => element.values == result);\n                fullScreenGestureMode =\n                    GlobalDataCache().fullScreenGestureMode.index;\n                setting.put(\n                    SettingBoxKey.fullScreenGestureMode, fullScreenGestureMode);\n                setState(() {});\n              }\n            },\n          ),\n          const SetSwitchItem(\n            title: '双击快退/快进',\n            subTitle: '左侧双击快退，右侧双击快进',\n            setKey: SettingBoxKey.enableQuickDouble,\n            defaultVal: true,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/pages/play_speed_set.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/pages/setting/widgets/switch_item.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/plugin/pl_player/models/play_speed.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass PlaySpeedPage extends StatefulWidget {\n  const PlaySpeedPage({super.key});\n\n  @override\n  State<PlaySpeedPage> createState() => _PlaySpeedPageState();\n}\n\nclass _PlaySpeedPageState extends State<PlaySpeedPage> {\n  Box videoStorage = GStrorage.video;\n  Box settingStorage = GStrorage.setting;\n  late double playSpeedDefault;\n  late List<double> playSpeedSystem;\n  late double longPressSpeedDefault;\n  late List customSpeedsList;\n  late bool enableAutoLongPressSpeed;\n  List<Map<dynamic, dynamic>> sheetMenu = [\n    {\n      'id': 1,\n      'title': '设置为默认倍速',\n      'leading': const Icon(\n        Icons.speed,\n        size: 21,\n      ),\n      'show': true,\n    },\n    {\n      'id': 2,\n      'title': '设置为默认长按倍速',\n      'leading': const Icon(\n        Icons.speed_sharp,\n        size: 21,\n      ),\n      'show': true,\n    },\n    {\n      'id': -1,\n      'title': '删除该项',\n      'leading': const Icon(\n        Icons.delete_outline,\n        size: 21,\n      ),\n      'show': true,\n    },\n  ];\n\n  @override\n  void initState() {\n    super.initState();\n    // 系统预设倍速\n    playSpeedSystem =\n        videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);\n    // 默认倍速\n    playSpeedDefault =\n        videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);\n    // 默认长按倍速\n    longPressSpeedDefault =\n        videoStorage.get(VideoBoxKey.longPressSpeedDefault, defaultValue: 2.0);\n    // 自定义倍速\n    customSpeedsList =\n        videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []);\n    enableAutoLongPressSpeed = settingStorage\n        .get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);\n    // 开启动态长按倍速时不展示\n    if (enableAutoLongPressSpeed) {\n      Map newItem = sheetMenu[1];\n      newItem['show'] = false;\n      setState(() {\n        sheetMenu[1] = newItem;\n      });\n    }\n  }\n\n  // 添加自定义倍速\n  void onAddSpeed() {\n    double customSpeed = 1.0;\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('添加倍速'),\n          content: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              // const Text('输入你想要的视频倍速，例如：1.0'),\n              const SizedBox(height: 12),\n              TextField(\n                keyboardType: TextInputType.number,\n                decoration: InputDecoration(\n                  labelText: '自定义倍速',\n                  border: OutlineInputBorder(\n                    borderRadius: BorderRadius.circular(6.0),\n                  ),\n                ),\n                onChanged: (e) {\n                  customSpeed = double.parse(e);\n                },\n              ),\n            ],\n          ),\n          actions: [\n            TextButton(\n              onPressed: () => SmartDialog.dismiss(),\n              child: const Text('取消'),\n            ),\n            TextButton(\n              onPressed: () async {\n                customSpeedsList.add(customSpeed);\n                await videoStorage.put(\n                    VideoBoxKey.customSpeedsList, customSpeedsList);\n                setState(() {});\n                SmartDialog.dismiss();\n              },\n              child: const Text('确认添加'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  // 设定倍速弹窗\n  void showBottomSheet(String type, int i) {\n    showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      builder: (BuildContext context) {\n        return Container(\n          padding: const EdgeInsets.only(top: 10),\n          child: ListView.builder(\n            shrinkWrap: true,\n            physics: const ClampingScrollPhysics(),\n            //重要\n            itemCount: sheetMenu.length,\n            itemBuilder: (BuildContext context, int index) {\n              return sheetMenu[index]['show']\n                  ? ListTile(\n                      onTap: () {\n                        Navigator.pop(context);\n                        menuAction(type, i, sheetMenu[index]['id']);\n                      },\n                      minLeadingWidth: 0,\n                      iconColor: Theme.of(context).colorScheme.onSurface,\n                      leading: sheetMenu[index]['leading'],\n                      title: Text(\n                        sheetMenu[index]['title'],\n                        style: Theme.of(context).textTheme.titleSmall,\n                      ),\n                    )\n                  : const SizedBox();\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  //\n  void menuAction(type, int index, id) async {\n    double chooseSpeed = 1.0;\n    // 获取当前选中的倍速值\n    chooseSpeed =\n        type == 'system' ? playSpeedSystem[index] : customSpeedsList[index];\n    // 设置\n    if (id == 1) {\n      // 设置默认倍速\n      playSpeedDefault = chooseSpeed;\n      videoStorage.put(VideoBoxKey.playSpeedDefault, playSpeedDefault);\n    } else if (id == 2) {\n      // 设置默认长按倍速\n      longPressSpeedDefault = chooseSpeed;\n      videoStorage.put(\n          VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);\n    } else if (id == -1) {\n      late List speedsList =\n          type == 'system' ? playSpeedSystem : customSpeedsList;\n      if (speedsList[index] == playSpeedDefault) {\n        SmartDialog.showToast('默认倍速不可删除');\n      }\n      if (speedsList[index] == longPressSpeedDefault) {\n        longPressSpeedDefault = 2.0;\n        videoStorage.put(\n            VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);\n      }\n      speedsList.removeAt(index);\n      await videoStorage.put(\n          type == 'system'\n              ? VideoBoxKey.playSpeedSystem\n              : VideoBoxKey.customSpeedsList,\n          speedsList);\n    }\n    setState(() {});\n    SmartDialog.showToast('操作成功');\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        elevation: 0,\n        scrolledUnderElevation: 0,\n        titleSpacing: 0,\n        centerTitle: false,\n        title: Text(\n          '倍速设置',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: SingleChildScrollView(\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Padding(\n              padding:\n                  const EdgeInsets.only(left: 14, right: 14, top: 6, bottom: 0),\n              child: Text(\n                '点击下方按钮设置默认（长按）倍速',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            ListTile(\n              dense: false,\n              title: const Text('默认倍速'),\n              subtitle: Text(playSpeedDefault.toString()),\n            ),\n            SetSwitchItem(\n              title: '动态长按倍速',\n              subTitle: '根据默认倍速长按时自动双倍',\n              setKey: SettingBoxKey.enableAutoLongPressSpeed,\n              defaultVal: enableAutoLongPressSpeed,\n              callFn: (val) {\n                Map newItem = sheetMenu[1];\n                val ? newItem['show'] = false : newItem['show'] = true;\n                setState(() {\n                  sheetMenu[1] = newItem;\n                  enableAutoLongPressSpeed = val;\n                });\n              },\n            ),\n            !enableAutoLongPressSpeed\n                ? ListTile(\n                    dense: false,\n                    title: const Text('默认长按倍速'),\n                    subtitle: Text(longPressSpeedDefault.toString()),\n                  )\n                : const SizedBox(),\n            if (playSpeedSystem.isNotEmpty) ...[\n              Padding(\n                padding: const EdgeInsets.only(\n                  left: 14,\n                  right: 14,\n                  bottom: 10,\n                  top: 20,\n                ),\n                child: Text(\n                  '系统预设倍速',\n                  style: Theme.of(context).textTheme.titleMedium,\n                ),\n              ),\n              Padding(\n                padding: const EdgeInsets.only(\n                  left: 18,\n                  right: 18,\n                  bottom: 30,\n                ),\n                child: Wrap(\n                  alignment: WrapAlignment.start,\n                  spacing: 8,\n                  runSpacing: 2,\n                  children: [\n                    for (int i = 0; i < playSpeedSystem.length; i++) ...[\n                      FilledButton.tonal(\n                        onPressed: () => showBottomSheet('system', i),\n                        child: Text(playSpeedSystem[i].toString()),\n                      ),\n                    ]\n                  ],\n                ),\n              )\n            ],\n            Padding(\n                padding: const EdgeInsets.only(\n                  left: 14,\n                  right: 14,\n                ),\n                child: Row(\n                  children: [\n                    Text(\n                      '自定义倍速',\n                      style: Theme.of(context).textTheme.titleMedium,\n                    ),\n                    const SizedBox(width: 12),\n                    TextButton(\n                      onPressed: () => onAddSpeed(),\n                      child: const Text('添加'),\n                    )\n                  ],\n                )),\n            Padding(\n              padding: EdgeInsets.only(\n                left: 18,\n                right: 18,\n                bottom: MediaQuery.of(context).padding.bottom + 40,\n              ),\n              child: customSpeedsList.isNotEmpty\n                  ? Wrap(\n                      alignment: WrapAlignment.start,\n                      spacing: 8,\n                      runSpacing: 2,\n                      children: [\n                        for (int i = 0; i < customSpeedsList.length; i++) ...[\n                          FilledButton.tonal(\n                            onPressed: () => showBottomSheet('custom', i),\n                            child: Text(customSpeedsList[i].toString()),\n                          ),\n                        ]\n                      ],\n                    )\n                  : SizedBox(\n                      height: 80,\n                      child: Center(\n                        child: Text(\n                          '未添加',\n                          style: TextStyle(\n                              color: Theme.of(context).colorScheme.outline),\n                        ),\n                      ),\n                    ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/play_setting.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/init.dart';\nimport 'package:pilipala/models/video/play/ao_output.dart';\nimport 'package:pilipala/models/video/play/quality.dart';\nimport 'package:pilipala/pages/setting/widgets/select_dialog.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/services/service_locator.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nimport '../../models/live/quality.dart';\nimport 'widgets/switch_item.dart';\n\nclass PlaySetting extends StatefulWidget {\n  const PlaySetting({super.key});\n\n  @override\n  State<PlaySetting> createState() => _PlaySettingState();\n}\n\nclass _PlaySettingState extends State<PlaySetting> {\n  Box setting = GStrorage.setting;\n  late dynamic defaultVideoQa;\n  late dynamic defaultLiveQa;\n  late dynamic defaultAudioQa;\n  late dynamic defaultDecode;\n  late int defaultFullScreenMode;\n  late int defaultBtmProgressBehavior;\n  late String defaultAoOutput;\n\n  @override\n  void initState() {\n    super.initState();\n    defaultVideoQa = setting.get(SettingBoxKey.defaultVideoQa,\n        defaultValue: VideoQuality.values.last.code);\n    defaultLiveQa = setting.get(SettingBoxKey.defaultLiveQa,\n        defaultValue: LiveQuality.values.last.code);\n    defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,\n        defaultValue: AudioQuality.values.last.code);\n    defaultDecode = setting.get(SettingBoxKey.defaultDecode,\n        defaultValue: VideoDecodeFormats.values.last.code);\n    defaultFullScreenMode = setting.get(SettingBoxKey.fullScreenMode,\n        defaultValue: FullScreenMode.values.first.code);\n    defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior,\n        defaultValue: BtmProgresBehavior.values.first.code);\n    defaultAoOutput =\n        setting.get(SettingBoxKey.defaultAoOutput, defaultValue: '0');\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n\n    // 重新验证媒体通知后台播放设置\n    videoPlayerServiceHandler.revalidateSetting();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;\n    TextStyle subTitleStyle = Theme.of(context)\n        .textTheme\n        .labelMedium!\n        .copyWith(color: Theme.of(context).colorScheme.outline);\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          '播放设置',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: ListView(\n        children: [\n          ListTile(\n            dense: false,\n            onTap: () => Get.toNamed('/playSpeedSet'),\n            title: Text('倍速设置', style: titleStyle),\n            subtitle: Text('设置视频播放速度', style: subTitleStyle),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () => Get.toNamed('/playerGestureSet'),\n            title: Text('手势设置', style: titleStyle),\n            subtitle: Text('设置播放器手势', style: subTitleStyle),\n          ),\n          const SetSwitchItem(\n            title: '开启1080P',\n            subTitle: '免登录查看1080P视频',\n            setKey: SettingBoxKey.p1080,\n            defaultVal: true,\n          ),\n          const SetSwitchItem(\n            title: 'CDN优化',\n            subTitle: '使用优质CDN线路',\n            setKey: SettingBoxKey.enableCDN,\n            defaultVal: true,\n          ),\n          const SetSwitchItem(\n            title: '自动播放',\n            subTitle: '进入详情页自动播放',\n            setKey: SettingBoxKey.autoPlayEnable,\n            defaultVal: true,\n          ),\n          const SetSwitchItem(\n            title: '后台播放',\n            subTitle: '进入后台时继续播放',\n            setKey: SettingBoxKey.enableBackgroundPlay,\n            defaultVal: false,\n          ),\n          if (Platform.isAndroid)\n            const SetSwitchItem(\n              title: '自动PiP播放',\n              subTitle: '进入后台时画中画播放',\n              setKey: SettingBoxKey.autoPiP,\n              defaultVal: false,\n            ),\n          const SetSwitchItem(\n            title: '自动全屏',\n            subTitle: '视频开始播放时进入全屏',\n            setKey: SettingBoxKey.enableAutoEnter,\n            defaultVal: false,\n          ),\n          const SetSwitchItem(\n            title: '自动退出',\n            subTitle: '视频结束播放时退出全屏',\n            setKey: SettingBoxKey.enableAutoExit,\n            defaultVal: false,\n          ),\n          const SetSwitchItem(\n            title: '开启硬解',\n            subTitle: '以较低功耗播放视频',\n            setKey: SettingBoxKey.enableHA,\n            defaultVal: false,\n          ),\n          const SetSwitchItem(\n            title: '观看人数',\n            subTitle: '展示同时在看人数',\n            setKey: SettingBoxKey.enableOnlineTotal,\n            defaultVal: false,\n          ),\n          const SetSwitchItem(\n            title: '亮度记忆',\n            subTitle: '返回时自动调整视频亮度',\n            setKey: SettingBoxKey.enableAutoBrightness,\n            defaultVal: false,\n          ),\n          const SetSwitchItem(\n            title: '弹幕开关',\n            subTitle: '展示弹幕',\n            setKey: SettingBoxKey.enableShowDanmaku,\n            defaultVal: false,\n          ),\n          SetSwitchItem(\n              title: '控制栏动画',\n              subTitle: '播放器控制栏显示动画效果',\n              setKey: SettingBoxKey.enablePlayerControlAnimation,\n              defaultVal: true,\n              callFn: (bool val) {\n                GlobalDataCache().enablePlayerControlAnimation = val;\n              }),\n          SetSwitchItem(\n            title: '港澳台模式',\n            setKey: SettingBoxKey.enableGATMode,\n            defaultVal: false,\n            callFn: (bool val) {\n              Request.setBaseUrl(type: val ? 'bangumi' : 'default');\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('默认视频画质', style: titleStyle),\n            subtitle: Text(\n              '当前默认画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '默认视频画质',\n                      value: defaultVideoQa,\n                      values: VideoQuality.values.reversed.map((e) {\n                        return {'title': e.description, 'value': e.code};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultVideoQa = result;\n                setting.put(SettingBoxKey.defaultVideoQa, result);\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('默认直播画质', style: titleStyle),\n            subtitle: Text(\n              '当前默认画质${LiveQualityCode.fromCode(defaultLiveQa)!.description!}',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '默认直播画质',\n                      value: defaultLiveQa,\n                      values: LiveQuality.values.reversed.map((e) {\n                        return {'title': e.description, 'value': e.code};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultLiveQa = result;\n                setting.put(SettingBoxKey.defaultLiveQa, result);\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('默认音质', style: titleStyle),\n            subtitle: Text(\n              '当前音质${AudioQualityCode.fromCode(defaultAudioQa)!.description!}',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '默认音质',\n                      value: defaultAudioQa,\n                      values: AudioQuality.values.reversed.map((e) {\n                        return {'title': e.description, 'value': e.code};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultAudioQa = result;\n                setting.put(SettingBoxKey.defaultAudioQa, result);\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('默认解码格式', style: titleStyle),\n            subtitle: Text(\n              '当前解码格式${VideoDecodeFormatsCode.fromCode(defaultDecode)!.description!}',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              String? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<String>(\n                      title: '默认解码格式',\n                      value: defaultDecode,\n                      values: VideoDecodeFormats.values.map((e) {\n                        return {'title': e.description, 'value': e.code};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultDecode = result;\n                setting.put(SettingBoxKey.defaultDecode, result);\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('音频输出方式', style: titleStyle),\n            subtitle: Text(\n              '当前输出方式 ${aoOutputList.firstWhere((element) => element['value'] == defaultAoOutput)['title']}',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              String? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<String>(\n                    title: '音频输出方式',\n                    value: defaultAoOutput,\n                    values: aoOutputList,\n                  );\n                },\n              );\n              if (result != null) {\n                defaultAoOutput = result;\n                setting.put(SettingBoxKey.defaultAoOutput, result);\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('默认全屏方式', style: titleStyle),\n            subtitle: Text(\n              '当前全屏方式：${FullScreenModeCode.fromCode(defaultFullScreenMode)!.description}',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '默认全屏方式',\n                      value: defaultFullScreenMode,\n                      values: FullScreenMode.values.map((e) {\n                        return {'title': e.description, 'value': e.code};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultFullScreenMode = result;\n                setting.put(SettingBoxKey.fullScreenMode, result);\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('底部进度条展示', style: titleStyle),\n            subtitle: Text(\n              '当前展示方式：${BtmProgresBehaviorCode.fromCode(defaultBtmProgressBehavior)!.description}',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '底部进度条展示',\n                      value: defaultBtmProgressBehavior,\n                      values: BtmProgresBehavior.values.map((e) {\n                        return {'title': e.description, 'value': e.code};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultBtmProgressBehavior = result;\n                setting.put(SettingBoxKey.btmProgressBehavior, result);\n                setState(() {});\n              }\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/privacy_setting.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass PrivacySetting extends StatefulWidget {\n  const PrivacySetting({super.key});\n\n  @override\n  State<PrivacySetting> createState() => _PrivacySettingState();\n}\n\nclass _PrivacySettingState extends State<PrivacySetting> {\n  bool userLogin = false;\n  Box userInfoCache = GStrorage.userInfo;\n  var userInfo;\n\n  @override\n  void initState() {\n    super.initState();\n    userInfo = userInfoCache.get('userInfoCache');\n    userLogin = userInfo != null;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;\n    TextStyle subTitleStyle = Theme.of(context)\n        .textTheme\n        .labelMedium!\n        .copyWith(color: Theme.of(context).colorScheme.outline);\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          '隐私设置',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: Column(\n        children: [\n          ListTile(\n            onTap: () {\n              if (!userLogin) {\n                SmartDialog.showToast('登录后查看');\n                return;\n              }\n              Get.toNamed('/blackListPage');\n            },\n            dense: false,\n            title: Text('黑名单管理', style: titleStyle),\n            subtitle: Text('已拉黑用户', style: subTitleStyle),\n          ),\n          ListTile(\n            onTap: () {\n              if (!userLogin) {\n                SmartDialog.showToast('请先登录');\n              }\n              MemberHttp.cookieToKey();\n            },\n            dense: false,\n            title: Text('刷新access_key', style: titleStyle),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/recommend_setting.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/common/rcmd_type.dart';\nimport 'package:pilipala/pages/setting/widgets/select_dialog.dart';\nimport 'package:pilipala/utils/recommend_filter.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nimport 'widgets/switch_item.dart';\n\nclass RecommendSetting extends StatefulWidget {\n  const RecommendSetting({super.key});\n\n  @override\n  State<RecommendSetting> createState() => _RecommendSettingState();\n}\n\nclass _RecommendSettingState extends State<RecommendSetting> {\n  Box setting = GStrorage.setting;\n  static Box localCache = GStrorage.localCache;\n  late dynamic defaultRcmdType;\n  Box userInfoCache = GStrorage.userInfo;\n  late dynamic userInfo;\n  bool userLogin = false;\n  late dynamic accessKeyInfo;\n  // late int filterUnfollowedRatio;\n  late int minDurationForRcmd;\n  late int minLikeRatioForRecommend;\n\n  @override\n  void initState() {\n    super.initState();\n    // 首页默认推荐类型\n    defaultRcmdType =\n        setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');\n    userInfo = userInfoCache.get('userInfoCache');\n    userLogin = userInfo != null;\n    accessKeyInfo = localCache.get(LocalCacheKey.accessKey, defaultValue: null);\n    // filterUnfollowedRatio = setting\n    //     .get(SettingBoxKey.filterUnfollowedRatio, defaultValue: 0);\n    minDurationForRcmd =\n        setting.get(SettingBoxKey.minDurationForRcmd, defaultValue: 0);\n    minLikeRatioForRecommend =\n        setting.get(SettingBoxKey.minLikeRatioForRecommend, defaultValue: 0);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;\n    TextStyle subTitleStyle = Theme.of(context)\n        .textTheme\n        .labelMedium!\n        .copyWith(color: Theme.of(context).colorScheme.outline);\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          '推荐设置',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: ListView(\n        children: [\n          ListTile(\n            dense: false,\n            title: Text('首页推荐类型', style: titleStyle),\n            subtitle: Text(\n              '当前使用「$defaultRcmdType端」推荐¹',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              String? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<String>(\n                    title: '推荐类型',\n                    value: defaultRcmdType,\n                    values: RcmdType.values.map((e) {\n                      return {'title': e.labels, 'value': e.values};\n                    }).toList(),\n                  );\n                },\n              );\n              if (result != null) {\n                if (result == 'app') {\n                  // app端推荐需要access_key\n                  if (accessKeyInfo == null) {\n                    if (!userLogin) {\n                      SmartDialog.showToast('请先登录');\n                      return;\n                    }\n                    // 显示一个确认框，告知用户可能会导致账号被风控\n                    SmartDialog.show(\n                        animationType: SmartAnimationType.centerFade_otherSlide,\n                        builder: (context) {\n                          return AlertDialog(\n                            title: const Text('提示'),\n                            content: const Text(\n                                '使用app端推荐需获取access_key，有小概率触发风控导致账号退出（在官方版本app重新登录即可解除），是否继续？'),\n                            actions: [\n                              TextButton(\n                                onPressed: () {\n                                  result = null;\n                                  SmartDialog.dismiss();\n                                },\n                                child: const Text('取消'),\n                              ),\n                              TextButton(\n                                onPressed: () async {\n                                  SmartDialog.dismiss();\n                                  await MemberHttp.cookieToKey();\n                                },\n                                child: const Text('确定'),\n                              ),\n                            ],\n                          );\n                        });\n                  }\n                }\n                if (result != null) {\n                  defaultRcmdType = result;\n                  setting.put(SettingBoxKey.defaultRcmdType, result);\n                  SmartDialog.showToast('下次启动时生效');\n                  setState(() {});\n                }\n              }\n            },\n          ),\n          const SetSwitchItem(\n            title: '推荐动态',\n            subTitle: '是否在推荐内容中展示动态(仅app端)',\n            setKey: SettingBoxKey.enableRcmdDynamic,\n            defaultVal: true,\n          ),\n          const SetSwitchItem(\n            title: '首页推荐刷新',\n            subTitle: '下拉刷新时保留上次内容',\n            setKey: SettingBoxKey.enableSaveLastData,\n            defaultVal: false,\n          ),\n          // 分割线\n          const Divider(height: 1),\n          ListTile(\n            dense: false,\n            title: Text('点赞率过滤', style: titleStyle),\n            subtitle: Text(\n              '过滤掉点赞数/播放量「小于$minLikeRatioForRecommend%」的推荐视频(仅web端)',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '选择点赞率（0即不过滤）',\n                      value: minLikeRatioForRecommend,\n                      values: [0, 1, 2, 3, 4].map((e) {\n                        return {'title': '$e %', 'value': e};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                minLikeRatioForRecommend = result;\n                setting.put(SettingBoxKey.minLikeRatioForRecommend, result);\n                RecommendFilter.update();\n                setState(() {});\n              }\n            },\n          ),\n          ListTile(\n            dense: false,\n            title: Text('视频时长过滤', style: titleStyle),\n            subtitle: Text(\n              '过滤掉时长「小于$minDurationForRcmd秒」的推荐视频',\n              style: subTitleStyle,\n            ),\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '选择时长（0即不过滤）',\n                      value: minDurationForRcmd,\n                      values: [0, 30, 60, 90, 120].map((e) {\n                        return {'title': '$e 秒', 'value': e};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                minDurationForRcmd = result;\n                setting.put(SettingBoxKey.minDurationForRcmd, result);\n                RecommendFilter.update();\n                setState(() {});\n              }\n            },\n          ),\n          SetSwitchItem(\n            title: '已关注Up豁免推荐过滤',\n            subTitle: '推荐中已关注用户发布的内容不会被过滤',\n            setKey: SettingBoxKey.exemptFilterForFollowed,\n            defaultVal: true,\n            callFn: (_) => {RecommendFilter.update},\n          ),\n          // ListTile(\n          //   dense: false,\n          //   title: Text('按比例过滤未关注Up', style: titleStyle),\n          //   subtitle: Text(\n          //     '滤除推荐中占比「$filterUnfollowedRatio%」的未关注用户发布的内容',\n          //     style: subTitleStyle,\n          //   ),\n          //   onTap: () async {\n          //     int? result = await showDialog(\n          //       context: context,\n          //       builder: (context) {\n          //         return SelectDialog<int>(\n          //             title: '选择滤除比例（0即不过滤）',\n          //             value: filterUnfollowedRatio,\n          //             values: [0, 16, 32, 48, 64].map((e) {\n          //               return {'title': '$e %', 'value': e};\n          //             }).toList());\n          //       },\n          //     );\n          //     if (result != null) {\n          //       filterUnfollowedRatio = result;\n          //       setting.put(\n          //           SettingBoxKey.filterUnfollowedRatio, result);\n          //       RecommendFilter.update();\n          //       setState(() {});\n          //     }\n          //   },\n          // ),\n          SetSwitchItem(\n            title: '过滤器也应用于相关视频',\n            subTitle: '视频详情页的相关视频也进行过滤²',\n            setKey: SettingBoxKey.applyFilterToRelatedVideos,\n            defaultVal: true,\n            callFn: (_) => {RecommendFilter.update},\n          ),\n          ListTile(\n            dense: true,\n            subtitle: Text(\n              '¹ 若默认web端推荐不太符合预期，可尝试切换至app端。\\n'\n              '¹ 选择“模拟未登录(notLogin)”，将以空的key请求推荐接口，但播放页仍会携带用户信息，保证账号能正常记录进度、点赞投币等。\\n\\n'\n              '² 由于接口未提供关注信息，无法豁免相关视频中的已关注Up。\\n\\n'\n              '* 其它（如热门视频、手动搜索、链接跳转等）均不受过滤器影响。\\n'\n              '* 设定较严苛的条件可导致推荐项数锐减或多次请求，请酌情选择。\\n'\n              '* 后续可能会增加更多过滤条件，敬请期待。',\n              style: Theme.of(context)\n                  .textTheme\n                  .labelSmall!\n                  .copyWith(color: Theme.of(context).colorScheme.outline.withOpacity(0.7)),\n            ),\n          )\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/style_setting.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/common/theme_type.dart';\nimport 'package:pilipala/pages/setting/pages/color_select.dart';\nimport 'package:pilipala/pages/setting/widgets/select_dialog.dart';\nimport 'package:pilipala/pages/setting/widgets/slide_dialog.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nimport '../../models/common/dynamic_badge_mode.dart';\nimport '../../models/common/nav_bar_config.dart';\nimport 'controller.dart';\nimport 'widgets/switch_item.dart';\n\nclass StyleSetting extends StatefulWidget {\n  const StyleSetting({super.key});\n\n  @override\n  State<StyleSetting> createState() => _StyleSettingState();\n}\n\nclass _StyleSettingState extends State<StyleSetting> {\n  final SettingController settingController = Get.put(SettingController());\n  final ColorSelectController colorSelectController =\n      Get.put(ColorSelectController());\n\n  Box setting = GStrorage.setting;\n  late int picQuality;\n  late ThemeType _tempThemeValue;\n  late dynamic defaultCustomRows;\n\n  @override\n  void initState() {\n    super.initState();\n    picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);\n    _tempThemeValue = settingController.themeType.value;\n    defaultCustomRows = setting.get(SettingBoxKey.customRows, defaultValue: 2);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;\n    TextStyle subTitleStyle = Theme.of(context)\n        .textTheme\n        .labelMedium!\n        .copyWith(color: Theme.of(context).colorScheme.outline);\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          '外观设置',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: ListView(\n        children: [\n          Obx(\n            () => ListTile(\n              enableFeedback: true,\n              onTap: () => settingController.onOpenFeedBack(),\n              title: const Text('震动反馈'),\n              subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle),\n              trailing: Transform.scale(\n                alignment: Alignment.centerRight,\n                scale: 0.8,\n                child: Switch(\n                    thumbIcon: MaterialStateProperty.resolveWith<Icon?>(\n                        (Set<MaterialState> states) {\n                      if (states.isNotEmpty &&\n                          states.first == MaterialState.selected) {\n                        return const Icon(Icons.done);\n                      }\n                      return null; // All other states will use the default thumbIcon.\n                    }),\n                    value: settingController.feedBackEnable.value,\n                    onChanged: (value) => settingController.onOpenFeedBack()),\n              ),\n            ),\n          ),\n          const SetSwitchItem(\n            title: 'MD3样式底栏',\n            subTitle: '符合Material You设计规范的底栏',\n            setKey: SettingBoxKey.enableMYBar,\n            defaultVal: true,\n          ),\n          const SetSwitchItem(\n            title: '首页顶栏收起',\n            subTitle: '首页列表滑动时，收起顶栏',\n            setKey: SettingBoxKey.hideSearchBar,\n            defaultVal: true,\n            needReboot: true,\n          ),\n          const SetSwitchItem(\n            title: '首页底栏收起',\n            subTitle: '首页列表滑动时，收起底栏',\n            setKey: SettingBoxKey.hideTabBar,\n            defaultVal: true,\n            needReboot: true,\n          ),\n          const SetSwitchItem(\n            title: '首页顶部背景渐变',\n            setKey: SettingBoxKey.enableGradientBg,\n            defaultVal: true,\n            needReboot: true,\n          ),\n          ListTile(\n            onTap: () async {\n              int? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<int>(\n                      title: '自定义列数',\n                      value: defaultCustomRows,\n                      values: [1, 2, 3, 4, 5].map((e) {\n                        return {'title': '$e 列', 'value': e};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                defaultCustomRows = result;\n                setting.put(SettingBoxKey.customRows, result);\n                setState(() {});\n              }\n            },\n            dense: false,\n            title: Text('自定义列数', style: titleStyle),\n            subtitle: Text(\n              '当前列数 $defaultCustomRows 列',\n              style: subTitleStyle,\n            ),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () {\n              showDialog(\n                context: context,\n                builder: (context) {\n                  return StatefulBuilder(\n                    builder: (context, StateSetter setState) {\n                      final SettingController settingController =\n                          Get.put(SettingController());\n                      return AlertDialog(\n                        title: const Text('图片质量'),\n                        contentPadding: const EdgeInsets.only(\n                            top: 20, left: 8, right: 8, bottom: 8),\n                        content: SizedBox(\n                          height: 40,\n                          child: Slider(\n                            value: picQuality.toDouble(),\n                            min: 10,\n                            max: 100,\n                            divisions: 9,\n                            label: '$picQuality%',\n                            onChanged: (double val) {\n                              picQuality = val.toInt();\n                              setState(() {});\n                            },\n                          ),\n                        ),\n                        actions: [\n                          TextButton(\n                              onPressed: () => Get.back(),\n                              child: Text('取消',\n                                  style: TextStyle(\n                                      color: Theme.of(context)\n                                          .colorScheme\n                                          .outline))),\n                          TextButton(\n                            onPressed: () {\n                              setting.put(\n                                  SettingBoxKey.defaultPicQa, picQuality);\n                              Get.back();\n                              settingController.picQuality.value = picQuality;\n                              GlobalDataCache().imgQuality = picQuality;\n                              SmartDialog.showToast('设置成功');\n                            },\n                            child: const Text('确定'),\n                          )\n                        ],\n                      );\n                    },\n                  );\n                },\n              );\n            },\n            title: Text('图片质量', style: titleStyle),\n            subtitle: Text('选择合适的图片清晰度，上限100%', style: subTitleStyle),\n            trailing: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 8.0),\n              child: Obx(\n                () => Text(\n                  '${settingController.picQuality.value}%',\n                  style: Theme.of(context).textTheme.titleSmall,\n                ),\n              ),\n            ),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () async {\n              double? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SlideDialog<double>(\n                    title: 'Toast不透明度',\n                    value: settingController.toastOpacity.value,\n                    min: 0.0,\n                    max: 1.0,\n                    divisions: 10,\n                  );\n                },\n              );\n              if (result != null) {\n                settingController.toastOpacity.value = result;\n                SmartDialog.showToast('设置成功');\n                setting.put(SettingBoxKey.defaultToastOp, result);\n              }\n            },\n            title: Text('Toast不透明度', style: titleStyle),\n            subtitle: Text('自定义Toast不透明度', style: subTitleStyle),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () async {\n              ThemeType? result = await showDialog(\n                context: context,\n                builder: (context) {\n                  return SelectDialog<ThemeType>(\n                      title: '主题模式',\n                      value: _tempThemeValue,\n                      values: ThemeType.values.map((e) {\n                        return {'title': e.description, 'value': e};\n                      }).toList());\n                },\n              );\n              if (result != null) {\n                _tempThemeValue = result;\n                settingController.themeType.value = result;\n                setting.put(SettingBoxKey.themeMode, result.code);\n                Get.forceAppUpdate();\n              }\n            },\n            title: Text('主题模式', style: titleStyle),\n            subtitle: Obx(() => Text(\n                '当前模式：${settingController.themeType.value.description}',\n                style: subTitleStyle)),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () => settingController.setDynamicBadgeMode(context),\n            title: Text('动态未读标记', style: titleStyle),\n            subtitle: Obx(() => Text(\n                '当前标记样式：${settingController.dynamicBadgeType.value.description}',\n                style: subTitleStyle)),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () => Get.toNamed('/colorSetting'),\n            title: Text('应用主题', style: titleStyle),\n            subtitle: Obx(() => Text(\n                '当前主题：${colorSelectController.type.value == 0 ? '动态取色' : '指定颜色'}',\n                style: subTitleStyle)),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () => settingController.seteDefaultHomePage(context),\n            title: Text('默认启动页', style: titleStyle),\n            subtitle: Obx(() => Text(\n                '当前启动页：${defaultNavigationBars.firstWhere((e) => e['id'] == settingController.defaultHomePage.value)['label']}',\n                style: subTitleStyle)),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () => Get.toNamed('/fontSizeSetting'),\n            title: Text('字体大小', style: titleStyle),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () => Get.toNamed('/tabbarSetting'),\n            title: Text('首页tabbar', style: titleStyle),\n          ),\n          ListTile(\n            dense: false,\n            onTap: () => Get.toNamed('/navbarSetting'),\n            title: Text('底部导航栏设置', style: titleStyle),\n          ),\n          // ListTile(\n          //   dense: false,\n          //   onTap: () => Get.toNamed('/actionMenuSet'),\n          //   title: Text('操作菜单设置', style: titleStyle),\n          // ),\n          if (Platform.isAndroid)\n            ListTile(\n              dense: false,\n              onTap: () => Get.toNamed('/displayModeSetting'),\n              title: Text('屏幕帧率', style: titleStyle),\n            ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/pages/setting/index.dart';\n\nclass SettingPage extends StatelessWidget {\n  const SettingPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final SettingController settingController = Get.put(SettingController());\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Text(\n          '设置',\n          style: Theme.of(context).textTheme.titleMedium,\n        ),\n      ),\n      body: Column(\n        children: [\n          ListTile(\n            onTap: () => Get.toNamed('/privacySetting'),\n            dense: false,\n            title: const Text('隐私设置'),\n          ),\n          ListTile(\n            onTap: () => Get.toNamed('/recommendSetting'),\n            dense: false,\n            title: const Text('推荐设置'),\n          ),\n          ListTile(\n            onTap: () => Get.toNamed('/playSetting'),\n            dense: false,\n            title: const Text('播放设置'),\n          ),\n          ListTile(\n            onTap: () => Get.toNamed('/styleSetting'),\n            dense: false,\n            title: const Text('外观设置'),\n          ),\n          ListTile(\n            onTap: () => Get.toNamed('/extraSetting'),\n            dense: false,\n            title: const Text('其他设置'),\n          ),\n          Obx(\n            () => Visibility(\n              visible: settingController.userLogin.value,\n              child: ListTile(\n                onTap: () => settingController.loginOut(),\n                dense: false,\n                title: const Text('退出登录'),\n              ),\n            ),\n          ),\n          ListTile(\n            onTap: () => Get.toNamed('/about'),\n            dense: false,\n            title: const Text('关于'),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/widgets/select_dialog.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass SelectDialog<T> extends StatefulWidget {\n  final T value;\n  final String title;\n  final List<dynamic> values;\n  const SelectDialog(\n      {super.key,\n      required this.value,\n      required this.values,\n      required this.title});\n\n  @override\n  _SelectDialogState<T> createState() => _SelectDialogState<T>();\n}\n\nclass _SelectDialogState<T> extends State<SelectDialog<T>> {\n  late T _tempValue;\n\n  @override\n  void initState() {\n    super.initState();\n    _tempValue = widget.value;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;\n\n    return AlertDialog(\n      title: Text(widget.title),\n      contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),\n      content: StatefulBuilder(builder: (context, StateSetter setState) {\n        return SingleChildScrollView(\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              for (var i in widget.values) ...[\n                RadioListTile(\n                  value: i['value'],\n                  title: Text(i['title'], style: titleStyle),\n                  groupValue: _tempValue,\n                  onChanged: (value) {\n                    setState(() {\n                      _tempValue = value as T;\n                    });\n                    Navigator.pop(context, _tempValue);\n                  },\n                ),\n              ]\n            ],\n          ),\n        );\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/widgets/select_item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/video/play/quality.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass SetSelectItem extends StatefulWidget {\n  final String? title;\n  final String? subTitle;\n  final String? setKey;\n  const SetSelectItem({\n    this.title,\n    this.subTitle,\n    this.setKey,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  State<SetSelectItem> createState() => _SetSelectItemState();\n}\n\nclass _SetSelectItemState extends State<SetSelectItem> {\n  Box setting = GStrorage.setting;\n  late dynamic currentVal;\n  late int currentIndex;\n  late List menus;\n  late List<PopupMenuEntry> popMenuItems;\n\n  @override\n  void initState() {\n    super.initState();\n    late String defaultVal;\n    switch (widget.setKey) {\n      case 'defaultVideoQa':\n        defaultVal = VideoQuality.values.last.description;\n        List<VideoQuality> list = menus = VideoQuality.values.reversed.toList();\n        currentVal = setting.get(widget.setKey, defaultValue: defaultVal);\n        currentIndex =\n            list.firstWhere((i) => i.description == currentVal).index;\n\n        popMenuItems = [\n          for (var i in list) ...[\n            PopupMenuItem(\n              value: i.code,\n              child: Text(i.description),\n            )\n          ]\n        ];\n\n        break;\n      case 'defaultAudioQa':\n        defaultVal = AudioQuality.values.last.description;\n        List<AudioQuality> list = menus = AudioQuality.values.reversed.toList();\n        currentVal = setting.get(widget.setKey, defaultValue: defaultVal);\n        currentIndex =\n            list.firstWhere((i) => i.description == currentVal).index;\n\n        popMenuItems = [\n          for (var i in list) ...[\n            PopupMenuItem(\n              value: i.index,\n              child: Text(i.description),\n            ),\n          ]\n        ];\n        break;\n      case 'defaultDecode':\n        defaultVal = VideoDecodeFormats.values[0].description;\n        currentVal = setting.get(widget.setKey, defaultValue: defaultVal);\n        List<VideoDecodeFormats> list = menus = VideoDecodeFormats.values;\n\n        currentIndex =\n            list.firstWhere((i) => i.description == currentVal).index;\n\n        popMenuItems = [\n          for (var i in list) ...[\n            PopupMenuItem(\n              value: i.index,\n              child: Text(i.description),\n            ),\n          ]\n        ];\n        break;\n      case 'defaultVideoSpeed':\n        defaultVal = '1.0';\n        currentVal = setting.get(widget.setKey, defaultValue: defaultVal);\n\n        break;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle subTitleStyle = Theme.of(context)\n        .textTheme\n        .labelMedium!\n        .copyWith(color: Theme.of(context).colorScheme.outline);\n    return ListTile(\n      onTap: () {},\n      dense: false,\n      title: Text(widget.title!),\n      subtitle: Text(\n        '当前${widget.title!} $currentVal',\n        style: subTitleStyle,\n      ),\n      trailing: PopupMenuButton(\n        initialValue: currentIndex,\n        icon: const Icon(\n          Icons.arrow_forward_rounded,\n          size: 22,\n        ),\n        onSelected: (item) {\n          currentVal = menus.firstWhere((e) => e.code == item).first;\n          setState(() {});\n        },\n        itemBuilder: (BuildContext context) =>\n            <PopupMenuEntry>[...popMenuItems],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/widgets/slide_dialog.dart",
    "content": "import 'package:flutter/material.dart';\n// import 'package:pilipala/models/common/theme_type.dart';\n\nclass SlideDialog<T extends num> extends StatefulWidget {\n  final T value;\n  final String title;\n  final double min;\n  final double max;\n  final int? divisions;\n  final String? suffix;\n\n  const SlideDialog({\n    super.key,\n    required this.value,\n    required this.title,\n    required this.min,\n    required this.max,\n    this.divisions,\n    this.suffix,\n  });\n\n  @override\n  _SlideDialogState<T> createState() => _SlideDialogState<T>();\n}\n\nclass _SlideDialogState<T extends num> extends State<SlideDialog<T>> {\n  late double _tempValue;\n\n  @override\n  void initState() {\n    super.initState();\n    _tempValue = widget.value.toDouble();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n      title: Text(widget.title),\n      contentPadding:\n          const EdgeInsets.only(top: 20, left: 8, right: 8, bottom: 8),\n      content: SizedBox(\n        height: 40,\n        child: Slider(\n          value: _tempValue,\n          min: widget.min,\n          max: widget.max,\n          divisions: widget.divisions ?? 10,\n          label: '$_tempValue${widget.suffix ?? ''}',\n          onChanged: (double value) {\n            setState(() {\n              _tempValue = value;\n            });\n          },\n        ),\n      ),\n      actions: [\n        TextButton(\n          onPressed: () => Navigator.pop(context),\n          child: Text(\n            '取消',\n            style: TextStyle(color: Theme.of(context).colorScheme.outline),\n          ),\n        ),\n        TextButton(\n          onPressed: () => Navigator.pop(context, _tempValue as T),\n          child: const Text('确定'),\n        )\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/setting/widgets/switch_item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass SetSwitchItem extends StatefulWidget {\n  final String? title;\n  final String? subTitle;\n  final String? setKey;\n  final bool? defaultVal;\n  final Function? callFn;\n  final bool? needReboot;\n\n  const SetSwitchItem({\n    this.title,\n    this.subTitle,\n    this.setKey,\n    this.defaultVal,\n    this.callFn,\n    this.needReboot,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  State<SetSwitchItem> createState() => _SetSwitchItemState();\n}\n\nclass _SetSwitchItemState extends State<SetSwitchItem> {\n  // ignore: non_constant_identifier_names\n  Box Setting = GStrorage.setting;\n  late bool val;\n\n  @override\n  void initState() {\n    super.initState();\n    val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false);\n  }\n\n  void switchChange(value) async {\n    val = value ?? !val;\n    await Setting.put(widget.setKey, val);\n    if (widget.setKey == SettingBoxKey.autoUpdate && value == true) {\n      Utils.checkUpdata();\n    }\n    if (widget.callFn != null) {\n      widget.callFn!.call(val);\n    }\n    if (widget.needReboot != null && widget.needReboot!) {\n      SmartDialog.showToast('重启生效');\n    }\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;\n    TextStyle subTitleStyle = Theme.of(context)\n        .textTheme\n        .labelMedium!\n        .copyWith(color: Theme.of(context).colorScheme.outline);\n    return ListTile(\n      enableFeedback: true,\n      onTap: () => switchChange(null),\n      title: Text(widget.title!, style: titleStyle),\n      subtitle: widget.subTitle != null\n          ? Text(widget.subTitle!, style: subTitleStyle)\n          : null,\n      trailing: Transform.scale(\n        alignment: Alignment.centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大\n        scale: 0.8,\n        child: Switch(\n          thumbIcon: MaterialStateProperty.resolveWith<Icon?>(\n              (Set<MaterialState> states) {\n            if (states.isNotEmpty && states.first == MaterialState.selected) {\n              return const Icon(Icons.done);\n            }\n            return null; // All other states will use the default thumbIcon.\n          }),\n          value: val,\n          onChanged: (val) => switchChange(val),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/subscription/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/user/info.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nimport '../../models/user/sub_folder.dart';\n\nclass SubController extends GetxController {\n  final ScrollController scrollController = ScrollController();\n  Rx<SubFolderModelData> subFolderData = SubFolderModelData().obs;\n  Box userInfoCache = GStrorage.userInfo;\n  UserInfoData? userInfo;\n  int currentPage = 1;\n  int pageSize = 20;\n  RxBool hasMore = true.obs;\n  late int mid;\n  late int ownerMid;\n  RxBool isOwner = false.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    mid = int.parse(Get.parameters['mid'] ?? '-1');\n    userInfo = userInfoCache.get('userInfoCache');\n    ownerMid = userInfo != null ? userInfo!.mid! : -1;\n    isOwner.value = mid == -1 || mid == ownerMid;\n  }\n\n  Future<dynamic> querySubFolder({type = 'init'}) async {\n    if (userInfo == null) {\n      return {'status': false, 'msg': '账号未登录', 'code': -101};\n    }\n    var res = await UserHttp.userSubFolder(\n      pn: currentPage,\n      ps: pageSize,\n      mid: isOwner.value ? ownerMid : mid,\n    );\n    if (res['status']) {\n      if (type == 'init') {\n        subFolderData.value = res['data'];\n      } else {\n        if (res['data'].list.isNotEmpty) {\n          subFolderData.value.list!.addAll(res['data'].list);\n          subFolderData.update((val) {});\n        }\n      }\n      currentPage++;\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n    return res;\n  }\n\n  Future onLoad() async {\n    querySubFolder(type: 'onload');\n  }\n\n  // 取消订阅\n  Future<void> cancelSub(SubFolderItemData subFolderItem) async {\n    showDialog(\n      context: Get.context!,\n      builder: (context) => AlertDialog(\n        title: const Text('提示'),\n        content: const Text('确定取消订阅吗？'),\n        actions: [\n          TextButton(\n            onPressed: () {\n              Get.back();\n            },\n            child: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n            ),\n          ),\n          TextButton(\n            onPressed: () async {\n              var res = await UserHttp.cancelSub(seasonId: subFolderItem.id!);\n              if (res['status']) {\n                subFolderData.value.list!.remove(subFolderItem);\n                subFolderData.update((val) {});\n                SmartDialog.showToast('取消订阅成功');\n              } else {\n                SmartDialog.showToast(res['msg']);\n              }\n              Get.back();\n            },\n            child: const Text('确定'),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/subscription/index.dart",
    "content": "library sub;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/subscription/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport 'controller.dart';\nimport 'widgets/item.dart';\n\nclass SubPage extends StatefulWidget {\n  const SubPage({super.key});\n\n  @override\n  State<SubPage> createState() => _SubPageState();\n}\n\nclass _SubPageState extends State<SubPage> {\n  final SubController _subController = Get.put(SubController());\n  late Future _futureBuilderFuture;\n  late ScrollController scrollController;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _subController.querySubFolder();\n    scrollController = _subController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('history', const Duration(seconds: 1), () {\n            _subController.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: false,\n        titleSpacing: 0,\n        title: Obx(() => Text(\n              '${_subController.isOwner.value ? '我' : 'Ta'}的订阅',\n              style: Theme.of(context).textTheme.titleMedium,\n            )),\n      ),\n      body: FutureBuilder(\n        future: _futureBuilderFuture,\n        builder: (context, snapshot) {\n          if (snapshot.connectionState == ConnectionState.done) {\n            Map? data = snapshot.data;\n            if (data != null && data['status']) {\n              if (_subController.subFolderData.value.list!.isNotEmpty) {\n                return Obx(\n                  () => ListView.builder(\n                    controller: scrollController,\n                    itemCount: _subController.subFolderData.value.list!.length,\n                    itemBuilder: (context, index) {\n                      return SubItem(\n                          subFolderItem:\n                              _subController.subFolderData.value.list![index],\n                          isOwner: _subController.isOwner.value,\n                          cancelSub: _subController.cancelSub);\n                    },\n                  ),\n                );\n              } else {\n                return const CustomScrollView(\n                  physics: NeverScrollableScrollPhysics(),\n                  slivers: [HttpError(errMsg: '', btnText: '没有数据', fn: null)],\n                );\n              }\n            } else {\n              return CustomScrollView(\n                physics: const NeverScrollableScrollPhysics(),\n                slivers: [\n                  HttpError(\n                    errMsg: data?['msg'] ?? '请求异常',\n                    btnText: data?['code'] == -101 ? '去登录' : null,\n                    fn: () {\n                      if (data?['code'] == -101) {\n                        RoutePush.loginRedirectPush();\n                      } else {\n                        setState(() {\n                          _futureBuilderFuture =\n                              _subController.querySubFolder();\n                        });\n                      }\n                    },\n                  ),\n                ],\n              );\n            }\n          } else {\n            // 骨架屏\n            return ListView.builder(\n              itemBuilder: (context, index) {\n                return const VideoCardHSkeleton();\n              },\n              itemCount: 10,\n            );\n          }\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/subscription/widgets/item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport '../../../models/user/sub_folder.dart';\n\nclass SubItem extends StatelessWidget {\n  final SubFolderItemData subFolderItem;\n  final bool isOwner;\n  final Function(SubFolderItemData) cancelSub;\n  const SubItem({\n    super.key,\n    required this.subFolderItem,\n    required this.isOwner,\n    required this.cancelSub,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    String heroTag = Utils.makeHeroTag(subFolderItem.id);\n    return InkWell(\n      onTap: () => Get.toNamed(\n        '/subDetail',\n        arguments: subFolderItem,\n        parameters: {\n          'heroTag': heroTag,\n          'seasonId': subFolderItem.id.toString(),\n          'type': subFolderItem.type.toString(),\n        },\n      ),\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(12, 7, 12, 7),\n        child: LayoutBuilder(\n          builder: (context, boxConstraints) {\n            double width =\n                (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;\n            return SizedBox(\n              height: width / StyleString.aspectRatio,\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.start,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  AspectRatio(\n                    aspectRatio: StyleString.aspectRatio,\n                    child: LayoutBuilder(\n                      builder: (context, boxConstraints) {\n                        double maxWidth = boxConstraints.maxWidth;\n                        double maxHeight = boxConstraints.maxHeight;\n                        return Hero(\n                          tag: heroTag,\n                          child: NetworkImgLayer(\n                            src: subFolderItem.cover,\n                            width: maxWidth,\n                            height: maxHeight,\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                  VideoContent(\n                    subFolderItem: subFolderItem,\n                    isOwner: isOwner,\n                    cancelSub: cancelSub,\n                  )\n                ],\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass VideoContent extends StatelessWidget {\n  final SubFolderItemData subFolderItem;\n  final bool isOwner;\n  final Function(SubFolderItemData)? cancelSub;\n  const VideoContent({\n    super.key,\n    required this.subFolderItem,\n    required this.isOwner,\n    this.cancelSub,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              subFolderItem.title!,\n              textAlign: TextAlign.start,\n              style: const TextStyle(\n                fontWeight: FontWeight.w500,\n                letterSpacing: 0.3,\n              ),\n            ),\n            const SizedBox(height: 2),\n            Text(\n              '合集 UP主：${subFolderItem.upper!.name!}',\n              textAlign: TextAlign.start,\n              style: TextStyle(\n                fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                color: Theme.of(context).colorScheme.outline,\n              ),\n            ),\n            const SizedBox(height: 2),\n            Text(\n              '${subFolderItem.mediaCount}个视频',\n              textAlign: TextAlign.start,\n              style: TextStyle(\n                fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                color: Theme.of(context).colorScheme.outline,\n              ),\n            ),\n            const Spacer(),\n            isOwner\n                ? Row(\n                    mainAxisAlignment: MainAxisAlignment.end,\n                    children: [\n                      IconButton(\n                        style: ButtonStyle(\n                          padding: MaterialStateProperty.all(EdgeInsets.zero),\n                        ),\n                        onPressed: () => cancelSub?.call(subFolderItem),\n                        icon: Icon(\n                          Icons.clear_outlined,\n                          color: Theme.of(context).colorScheme.outline,\n                          size: 18,\n                        ),\n                      )\n                    ],\n                  )\n                : const SizedBox()\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/subscription_detail/controller.dart",
    "content": "import 'package:get/get.dart';\nimport 'package:pilipala/http/user.dart';\n\nimport '../../models/user/sub_detail.dart';\nimport '../../models/user/sub_folder.dart';\n\nclass SubDetailController extends GetxController {\n  late SubFolderItemData item;\n  late int seasonId;\n  late String heroTag;\n  int currentPage = 1;\n  bool isLoadingMore = false;\n  Rx<DetailInfo> subInfo = DetailInfo().obs;\n  RxList<SubDetailMediaItem> subList = <SubDetailMediaItem>[].obs;\n  RxString loadingText = '加载中...'.obs;\n  int mediaCount = 0;\n  late int channelType;\n\n  @override\n  void onInit() {\n    item = Get.arguments;\n    final parameters = Get.parameters;\n    if (parameters.isNotEmpty) {\n      seasonId = int.tryParse(parameters['seasonId'] ?? '') ?? 0;\n      heroTag = parameters['heroTag'] ?? '';\n      channelType = int.tryParse(parameters['type'] ?? '') ?? 0;\n    }\n    super.onInit();\n  }\n\n  Future<dynamic> queryUserSeasonList({type = 'init'}) async {\n    if (type == 'onLoad' && subList.length >= mediaCount) {\n      loadingText.value = '没有更多了';\n      return;\n    }\n    isLoadingMore = true;\n    var res = channelType == 21\n        ? await UserHttp.userSeasonList(\n            seasonId: seasonId,\n            ps: 20,\n            pn: currentPage,\n          )\n        : await UserHttp.userResourceList(\n            seasonId: seasonId,\n            ps: 20,\n            pn: currentPage,\n          );\n    if (res['status']) {\n      subInfo.value = res['data'].info;\n      if (currentPage == 1 && type == 'init') {\n        subList.value = res['data'].medias;\n        mediaCount = res['data'].info.mediaCount;\n      } else if (type == 'onLoad') {\n        subList.addAll(res['data'].medias);\n      }\n      if (subList.length >= mediaCount) {\n        loadingText.value = '没有更多了';\n      }\n    }\n    currentPage += 1;\n    isLoadingMore = false;\n    return res;\n  }\n\n  onLoad() {\n    queryUserSeasonList(type: 'onLoad');\n  }\n}\n"
  },
  {
    "path": "lib/pages/subscription_detail/index.dart",
    "content": "library sub_detail;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/subscription_detail/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/no_data.dart';\n\nimport '../../models/user/sub_folder.dart';\nimport '../../utils/utils.dart';\nimport 'controller.dart';\nimport 'widget/sub_video_card.dart';\n\nclass SubDetailPage extends StatefulWidget {\n  const SubDetailPage({super.key});\n\n  @override\n  State<SubDetailPage> createState() => _SubDetailPageState();\n}\n\nclass _SubDetailPageState extends State<SubDetailPage> {\n  late final ScrollController _controller = ScrollController();\n  final SubDetailController _subDetailController =\n      Get.put(SubDetailController());\n  late StreamController<bool> titleStreamC =\n      StreamController<bool>.broadcast(); // a\n  late Future _futureBuilderFuture;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _subDetailController.queryUserSeasonList();\n    _controller.addListener(\n      () {\n        if (_controller.offset > 160) {\n          titleStreamC.add(true);\n        } else if (_controller.offset <= 160) {\n          titleStreamC.add(false);\n        }\n\n        if (_controller.position.pixels >=\n            _controller.position.maxScrollExtent - 200) {\n          EasyThrottle.throttle('subDetail', const Duration(seconds: 1), () {\n            _subDetailController.onLoad();\n          });\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    titleStreamC.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: CustomScrollView(\n        controller: _controller,\n        slivers: [\n          SliverAppBar(\n            expandedHeight: 260 - MediaQuery.of(context).padding.top,\n            pinned: true,\n            titleSpacing: 0,\n            title: StreamBuilder(\n              stream: titleStreamC.stream.distinct(),\n              initialData: false,\n              builder: (context, AsyncSnapshot snapshot) {\n                return AnimatedOpacity(\n                  opacity: snapshot.data ? 1 : 0,\n                  curve: Curves.easeOut,\n                  duration: const Duration(milliseconds: 500),\n                  child: Row(\n                    children: [\n                      Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          Text(\n                            _subDetailController.item.title!,\n                            style: Theme.of(context).textTheme.titleMedium,\n                          ),\n                          Text(\n                            '共${_subDetailController.item.mediaCount!}条视频',\n                            style: Theme.of(context).textTheme.labelMedium,\n                          )\n                        ],\n                      )\n                    ],\n                  ),\n                );\n              },\n            ),\n            flexibleSpace: FlexibleSpaceBar(\n              background: Container(\n                decoration: BoxDecoration(\n                  border: Border(\n                    bottom: BorderSide(\n                      color: Theme.of(context).dividerColor.withOpacity(0.2),\n                    ),\n                  ),\n                ),\n                padding: EdgeInsets.only(\n                    top: kTextTabBarHeight +\n                        MediaQuery.of(context).padding.top +\n                        30,\n                    left: 20,\n                    right: 20),\n                child: SizedBox(\n                  height: 200,\n                  child: Row(\n                    // mainAxisAlignment: MainAxisAlignment.center,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Hero(\n                        tag: _subDetailController.heroTag,\n                        child: NetworkImgLayer(\n                          width: 180,\n                          height: 110,\n                          src: _subDetailController.item.cover,\n                        ),\n                      ),\n                      const SizedBox(width: 14),\n                      Expanded(\n                        child: Column(\n                          mainAxisAlignment: MainAxisAlignment.start,\n                          crossAxisAlignment: CrossAxisAlignment.start,\n                          children: [\n                            const SizedBox(height: 4),\n                            Text(\n                              _subDetailController.item.title!,\n                              style: TextStyle(\n                                  fontSize: Theme.of(context)\n                                      .textTheme\n                                      .titleMedium!\n                                      .fontSize,\n                                  fontWeight: FontWeight.bold),\n                            ),\n                            const SizedBox(height: 4),\n                            GestureDetector(\n                              onTap: () {\n                                SubFolderItemData item =\n                                    _subDetailController.item;\n                                Get.toNamed(\n                                  '/member?mid=${item.upper!.mid}',\n                                  arguments: {\n                                    'face': item.upper!.face,\n                                  },\n                                );\n                              },\n                              child: Text(\n                                _subDetailController.item.upper!.name!,\n                                style: TextStyle(\n                                    color:\n                                        Theme.of(context).colorScheme.primary),\n                              ),\n                            ),\n                            const SizedBox(height: 4),\n                            Obx(\n                              () => Text(\n                                '${Utils.numFormat(_subDetailController.subInfo.value.cntInfo?['play'])}次播放',\n                                style: TextStyle(\n                                    fontSize: Theme.of(context)\n                                        .textTheme\n                                        .labelSmall!\n                                        .fontSize,\n                                    color:\n                                        Theme.of(context).colorScheme.outline),\n                              ),\n                            )\n                          ],\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n          ),\n          SliverToBoxAdapter(\n            child: Padding(\n              padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),\n              child: Text(\n                '共${_subDetailController.item.mediaCount}条视频',\n                style: TextStyle(\n                  fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,\n                  color: Theme.of(context).colorScheme.outline,\n                  letterSpacing: 1,\n                ),\n              ),\n            ),\n          ),\n          FutureBuilder(\n            future: _futureBuilderFuture,\n            builder: (context, snapshot) {\n              if (snapshot.connectionState == ConnectionState.done) {\n                Map? data = snapshot.data;\n                if (data != null && data['status']) {\n                  if (_subDetailController.item.mediaCount == 0) {\n                    return const NoData();\n                  } else {\n                    List subList = _subDetailController.subList;\n                    return Obx(\n                      () => subList.isEmpty\n                          ? const SliverToBoxAdapter(child: SizedBox())\n                          : SliverList(\n                              delegate:\n                                  SliverChildBuilderDelegate((context, index) {\n                                return SubVideoCardH(\n                                  videoItem: subList[index],\n                                );\n                              }, childCount: subList.length),\n                            ),\n                    );\n                  }\n                } else {\n                  return HttpError(\n                    errMsg: data?['msg'] ?? '请求异常',\n                    fn: () => setState(() {}),\n                  );\n                }\n              } else {\n                // 骨架屏\n                return SliverList(\n                  delegate: SliverChildBuilderDelegate((context, index) {\n                    return const VideoCardHSkeleton();\n                  }, childCount: 10),\n                );\n              }\n            },\n          ),\n          SliverToBoxAdapter(\n            child: Container(\n              height: MediaQuery.of(context).padding.bottom + 60,\n              padding: EdgeInsets.only(\n                  bottom: MediaQuery.of(context).padding.bottom),\n              child: Center(\n                child: Obx(\n                  () => Text(\n                    _subDetailController.loadingText.value,\n                    style: TextStyle(\n                        color: Theme.of(context).colorScheme.outline,\n                        fontSize: 13),\n                  ),\n                ),\n              ),\n            ),\n          )\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/subscription_detail/widget/sub_video_card.dart",
    "content": "import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/stat/danmu.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/utils/image_save.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport '../../../common/widgets/badge.dart';\nimport '../../../models/user/sub_detail.dart';\n\n// 收藏视频卡片 - 水平布局\nclass SubVideoCardH extends StatelessWidget {\n  final SubDetailMediaItem videoItem;\n  final int? searchType;\n\n  const SubVideoCardH({\n    Key? key,\n    required this.videoItem,\n    this.searchType,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    int id = videoItem.id!;\n    String bvid = videoItem.bvid!;\n    String heroTag = Utils.makeHeroTag(id);\n    return InkWell(\n      onTap: () async {\n        int cid = await SearchHttp.ab2c(bvid: bvid);\n        Map<String, String> parameters = {\n          'bvid': bvid,\n          'cid': cid.toString(),\n        };\n\n        Get.toNamed('/video', parameters: parameters, arguments: {\n          'videoItem': videoItem,\n          'heroTag': heroTag,\n          'videoType': SearchType.video,\n        });\n      },\n      onLongPress: () => imageSaveDialog(\n        context,\n        videoItem,\n        SmartDialog.dismiss,\n      ),\n      child: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.fromLTRB(\n                StyleString.safeSpace, 5, StyleString.safeSpace, 5),\n            child: LayoutBuilder(\n              builder: (context, boxConstraints) {\n                double width =\n                    (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;\n                return SizedBox(\n                  height: width / StyleString.aspectRatio,\n                  child: Row(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      AspectRatio(\n                        aspectRatio: StyleString.aspectRatio,\n                        child: LayoutBuilder(\n                          builder: (context, boxConstraints) {\n                            double maxWidth = boxConstraints.maxWidth;\n                            double maxHeight = boxConstraints.maxHeight;\n                            return Stack(\n                              children: [\n                                Hero(\n                                  tag: heroTag,\n                                  child: NetworkImgLayer(\n                                    src: videoItem.cover,\n                                    width: maxWidth,\n                                    height: maxHeight,\n                                  ),\n                                ),\n                                PBadge(\n                                  text: Utils.timeFormat(videoItem.duration!),\n                                  right: 6.0,\n                                  bottom: 6.0,\n                                  type: 'gray',\n                                ),\n                                // if (videoItem.ogv != null) ...[\n                                //   PBadge(\n                                //     text: videoItem.ogv['type_name'],\n                                //     top: 6.0,\n                                //     right: 6.0,\n                                //     bottom: null,\n                                //     left: null,\n                                //   ),\n                                // ],\n                              ],\n                            );\n                          },\n                        ),\n                      ),\n                      VideoContent(\n                        videoItem: videoItem,\n                        searchType: searchType,\n                      )\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass VideoContent extends StatelessWidget {\n  final dynamic videoItem;\n  final int? searchType;\n  const VideoContent({\n    super.key,\n    required this.videoItem,\n    this.searchType,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: Padding(\n        padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),\n        child: Stack(\n          children: [\n            Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                  videoItem.title,\n                  textAlign: TextAlign.start,\n                  style: const TextStyle(\n                    fontWeight: FontWeight.w500,\n                    letterSpacing: 0.3,\n                  ),\n                  maxLines: 2,\n                  overflow: TextOverflow.ellipsis,\n                ),\n                const Spacer(),\n                Text(\n                  Utils.dateFormat(videoItem.pubtime),\n                  style: TextStyle(\n                      fontSize: 11,\n                      color: Theme.of(context).colorScheme.outline),\n                ),\n                Padding(\n                  padding: const EdgeInsets.only(top: 2),\n                  child: Row(\n                    children: [\n                      StatView(view: videoItem.cntInfo['play']),\n                      const SizedBox(width: 8),\n                      StatDanMu(danmu: videoItem.cntInfo['danmaku']),\n                      const Spacer(),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/README.md",
    "content": "视频详情页预渲染\n+ videoItem \n  + title\n  + stat\n    + view \n    + danmaku\n  + pubdate\n  + owner\n    + face\n    + name"
  },
  {
    "path": "lib/pages/video/detail/controller.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'package:floating/floating.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:ns_danmaku/ns_danmaku.dart';\nimport 'package:pilipala/http/constants.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/models/video/later.dart';\nimport 'package:pilipala/models/video/play/quality.dart';\nimport 'package:pilipala/models/video/play/url.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/pages/video/detail/reply_reply/index.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'package:pilipala/utils/video_utils.dart';\nimport 'package:screen_brightness/screen_brightness.dart';\n\nimport '../../../models/video/subTitile/content.dart';\nimport '../../../http/danmaku.dart';\nimport '../../../plugin/pl_player/models/bottom_control_type.dart';\nimport '../../../utils/id_utils.dart';\nimport 'introduction/controller.dart';\nimport 'reply/controller.dart';\nimport 'widgets/header_control.dart';\nimport 'widgets/watch_later_list.dart';\n\nclass VideoDetailController extends GetxController\n    with GetSingleTickerProviderStateMixin {\n  /// 路由传参\n  String bvid = Get.parameters['bvid']!;\n  RxInt cid = int.parse(Get.parameters['cid']!).obs;\n  RxInt danmakuCid = 0.obs;\n  String heroTag = Get.arguments['heroTag'];\n  // 视频详情\n  Map videoItem = {};\n  // 视频类型 默认投稿视频\n  SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;\n  // 页面来源 稍后再看 收藏夹\n  RxString sourceType = 'normal'.obs;\n\n  /// tabs相关配置\n  late TabController tabCtr;\n  RxList<String> tabs = <String>['简介', '评论'].obs;\n\n  // 请求返回的视频信息\n  late PlayUrlModel data;\n  // 请求状态\n  RxBool isLoading = false.obs;\n\n  /// 播放器配置 画质 音质 解码格式\n  late VideoQuality currentVideoQa;\n  AudioQuality? currentAudioQa;\n  VideoDecodeFormats? currentDecodeFormats;\n  // 是否开始自动播放 存在多p的情况下，第二p需要为true\n  RxBool autoPlay = true.obs;\n  // 视频资源是否有效\n  RxBool isEffective = true.obs;\n  // 封面图的展示\n  RxBool isShowCover = true.obs;\n  // 硬解\n  RxBool enableHA = false.obs;\n\n  /// 本地存储\n  Box userInfoCache = GStrorage.userInfo;\n  Box localCache = GStrorage.localCache;\n  Box setting = GStrorage.setting;\n\n  RxInt oid = 0.obs;\n  // 评论id 请求楼中楼评论使用\n  int fRpid = 0;\n\n  ReplyItemModel? firstFloor;\n  final scaffoldKey = GlobalKey<ScaffoldState>();\n  RxString bgCover = ''.obs;\n  RxString cover = ''.obs;\n  PlPlayerController plPlayerController = PlPlayerController();\n\n  late VideoItem firstVideo;\n  late AudioItem firstAudio;\n  late String videoUrl;\n  late String audioUrl;\n  late Duration defaultST;\n  // 亮度\n  double? brightness;\n  // 默认记录历史记录\n  bool enableHeart = true;\n  var userInfo;\n  late bool isFirstTime = true;\n  Floating? floating;\n  late PreferredSizeWidget headerControl;\n\n  late bool enableCDN;\n  late int? cacheVideoQa;\n  late String cacheDecode;\n  late int defaultAudioQa;\n\n  PersistentBottomSheetController? replyReplyBottomSheetCtr;\n  RxList<SubTitileContentModel> subtitleContents =\n      <SubTitileContentModel>[].obs;\n  late bool enableRelatedVideo;\n  List subtitles = [];\n  RxList<BottomControlType> bottomList = [\n    BottomControlType.playOrPause,\n    BottomControlType.time,\n    BottomControlType.space,\n    BottomControlType.fit,\n    BottomControlType.fullscreen,\n  ].obs;\n  RxDouble sheetHeight = 0.0.obs;\n  RxString archiveSourceType = 'dash'.obs;\n  ScrollController? replyScrollController;\n  List<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[];\n  RxBool isWatchLaterVisible = false.obs;\n  RxString watchLaterTitle = ''.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    final Map argMap = Get.arguments;\n    userInfo = userInfoCache.get('userInfoCache');\n    if (argMap.containsKey('videoItem')) {\n      var args = argMap['videoItem'];\n      updateCover(args.pic);\n    } else if (argMap.containsKey('pic')) {\n      updateCover(argMap['pic']);\n    }\n\n    tabCtr = TabController(length: 2, vsync: this);\n    autoPlay.value =\n        setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);\n    enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: false);\n    enableRelatedVideo =\n        setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true);\n    if (userInfo == null ||\n        localCache.get(LocalCacheKey.historyPause) == true) {\n      enableHeart = false;\n    }\n    danmakuCid.value = cid.value;\n\n    ///\n    if (Platform.isAndroid) {\n      floating = Floating();\n    }\n\n    // CDN优化\n    enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);\n    // 预设的画质\n    cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa);\n    // 预设的解码格式\n    cacheDecode = setting.get(SettingBoxKey.defaultDecode,\n        defaultValue: VideoDecodeFormats.values.last.code);\n    defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,\n        defaultValue: AudioQuality.hiRes.code);\n    oid.value = IdUtils.bv2av(Get.parameters['bvid']!);\n    getSubtitle();\n    headerControl = HeaderControl(\n      controller: plPlayerController,\n      videoDetailCtr: this,\n      floating: floating,\n      bvid: bvid,\n      videoType: videoType,\n    );\n\n    sourceType.value = argMap['sourceType'] ?? 'normal';\n    isWatchLaterVisible.value =\n        sourceType.value == 'watchLater' || sourceType.value == 'fav';\n    if (sourceType.value == 'watchLater') {\n      watchLaterTitle.value = '稍后再看';\n      fetchMediaList();\n    }\n    if (sourceType.value == 'fav') {\n      watchLaterTitle.value = argMap['favTitle'];\n      queryFavVideoList();\n    }\n    tabCtr.addListener(() {\n      onTabChanged();\n    });\n  }\n\n  showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {\n    replyReplyBottomSheetCtr =\n        scaffoldKey.currentState?.showBottomSheet((BuildContext context) {\n      return VideoReplyReplyPanel(\n        oid: oid,\n        rpid: fRpid,\n        closePanel: () => {\n          fRpid = 0,\n        },\n        firstFloor: firstFloor,\n        replyType: ReplyType.video,\n        source: 'videoDetail',\n        sheetHeight: sheetHeight.value,\n        currentReply: currentReply,\n        loadMore: loadMore,\n      );\n    });\n    replyReplyBottomSheetCtr?.closed.then((value) {\n      fRpid = 0;\n    });\n  }\n\n  /// 更新画质、音质\n  /// TODO 继续进度播放\n  updatePlayer() {\n    defaultST = plPlayerController.position.value;\n    plPlayerController.removeListeners();\n    plPlayerController.isBuffering.value = false;\n    plPlayerController.buffered.value = Duration.zero;\n\n    if (archiveSourceType.value == 'dash') {\n      /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl\n      List<VideoItem> videoList =\n          data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();\n      try {\n        firstVideo = videoList.firstWhere(\n            (i) => i.codecs!.startsWith(currentDecodeFormats?.code));\n      } catch (_) {\n        if (currentVideoQa == VideoQuality.dolbyVision) {\n          firstVideo = videoList.first;\n          currentDecodeFormats =\n              VideoDecodeFormatsCode.fromString(videoList.first.codecs!)!;\n        } else {\n          // 当前格式不可用\n          currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(\n              SettingBoxKey.defaultDecode,\n              defaultValue: VideoDecodeFormats.values.last.code))!;\n          firstVideo = videoList.firstWhere(\n              (i) => i.codecs!.startsWith(currentDecodeFormats?.code));\n        }\n      }\n      videoUrl = firstVideo.baseUrl!;\n\n      /// 根据currentAudioQa 重新设置audioUrl\n      if (currentAudioQa != null) {\n        final AudioItem firstAudio = data.dash!.audio!.firstWhere(\n          (AudioItem i) => i.id == currentAudioQa!.code,\n          orElse: () => data.dash!.audio!.first,\n        );\n        audioUrl = firstAudio.baseUrl ?? '';\n      }\n    }\n\n    if (archiveSourceType.value == 'durl') {\n      cacheVideoQa = VideoQualityCode.toCode(currentVideoQa);\n      queryVideoUrl();\n    }\n    playerInit();\n  }\n\n  Future playerInit({\n    video,\n    audio,\n    seekToTime,\n    duration,\n    bool? autoplay,\n  }) async {\n    /// 设置/恢复 屏幕亮度\n    if (brightness != null) {\n      ScreenBrightness().setScreenBrightness(brightness!);\n    } else {\n      ScreenBrightness().resetScreenBrightness();\n    }\n    await plPlayerController.setDataSource(\n      DataSource(\n        videoSource: video ?? videoUrl,\n        audioSource: audio ?? audioUrl,\n        type: DataSourceType.network,\n        httpHeaders: {\n          'user-agent':\n              'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',\n          'referer': HttpString.baseUrl\n        },\n      ),\n      // 硬解\n      enableHA: enableHA.value,\n      seekTo: seekToTime ?? defaultST,\n      duration: duration ?? Duration(milliseconds: data.timeLength ?? 0),\n      // 宽>高 水平 否则 垂直\n      direction: firstVideo.width != null && firstVideo.height != null\n          ? ((firstVideo.width! - firstVideo.height!) > 0\n              ? 'horizontal'\n              : 'vertical')\n          : null,\n      bvid: bvid,\n      cid: cid.value,\n      enableHeart: enableHeart,\n      isFirstTime: isFirstTime,\n      autoplay: autoplay ?? autoPlay.value,\n    );\n\n    /// 开启自动全屏时，在player初始化完成后立即传入headerControl\n    plPlayerController.headerControl = headerControl;\n\n    plPlayerController.subtitles.value = subtitles;\n  }\n\n  // 视频链接\n  Future queryVideoUrl() async {\n    var result =\n        await VideoHttp.videoUrl(cid: cid.value, bvid: bvid, qn: cacheVideoQa);\n    if (result['status']) {\n      data = result['data'];\n      if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) {\n        SmartDialog.showToast(\n          '该视频为专属视频，仅提供试看',\n          displayTime: const Duration(seconds: 3),\n        );\n        videoUrl = data.durl!.first.url!;\n        audioUrl = '';\n        defaultST = Duration.zero;\n        firstVideo = VideoItem();\n        if (autoPlay.value) {\n          await playerInit();\n          isShowCover.value = false;\n        }\n        return result;\n      }\n      if (data.durl != null) {\n        archiveSourceType.value = 'durl';\n        videoUrl = data.durl!.first.url!;\n        audioUrl = '';\n        defaultST = Duration.zero;\n        firstVideo = VideoItem();\n        currentVideoQa = VideoQualityCode.fromCode(data.quality!)!;\n        if (autoPlay.value) {\n          await playerInit();\n          isShowCover.value = false;\n        }\n        return result;\n      }\n      final List<VideoItem> allVideosList = data.dash!.video!;\n      try {\n        archiveSourceType.value = 'dash';\n        // 当前可播放的最高质量视频\n        int currentHighVideoQa = allVideosList.first.quality!.code;\n        // 预设的画质为null，则当前可用的最高质量\n        cacheVideoQa ??= currentHighVideoQa;\n        int resVideoQa = currentHighVideoQa;\n        if (cacheVideoQa! <= currentHighVideoQa) {\n          // 如果预设的画质低于当前最高\n          final List<int> numbers = data.acceptQuality!\n              .where((e) => e <= currentHighVideoQa)\n              .toList();\n          resVideoQa = Utils.findClosestNumber(cacheVideoQa!, numbers);\n        }\n        currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;\n\n        /// 取出符合当前画质的videoList\n        final List<VideoItem> videosList =\n            allVideosList.where((e) => e.quality!.code == resVideoQa).toList();\n\n        /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式\n        final List<FormatItem> supportFormats = data.supportFormats!;\n        // 根据画质选编码格式\n        final List supportDecodeFormats =\n            supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;\n        // 默认从设置中取AVC\n        currentDecodeFormats = VideoDecodeFormatsCode.fromString(cacheDecode)!;\n        try {\n          // 当前视频没有对应格式返回第一个\n          bool flag = false;\n          for (var i in supportDecodeFormats) {\n            if (i.startsWith(currentDecodeFormats?.code)) {\n              flag = true;\n            }\n          }\n          currentDecodeFormats = flag\n              ? currentDecodeFormats\n              : VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!;\n        } catch (err) {\n          SmartDialog.showToast('DecodeFormats error: $err');\n        }\n\n        /// 取出符合当前解码格式的videoItem\n        try {\n          firstVideo = videosList.firstWhere(\n              (e) => e.codecs!.startsWith(currentDecodeFormats?.code));\n        } catch (_) {\n          firstVideo = videosList.first;\n        }\n        videoUrl = enableCDN\n            ? VideoUtils.getCdnUrl(firstVideo)\n            : (firstVideo.backupUrl ?? firstVideo.baseUrl!);\n      } catch (err) {\n        SmartDialog.showToast('firstVideo error: $err');\n      }\n\n      /// 优先顺序 设置中指定质量 -> 当前可选的最高质量\n      late AudioItem? firstAudio;\n      final List<AudioItem> audiosList = data.dash!.audio!;\n\n      try {\n        if (data.dash!.dolby?.audio?.isNotEmpty == true) {\n          // 杜比\n          audiosList.insert(0, data.dash!.dolby!.audio!.first);\n        }\n\n        if (data.dash!.flac?.audio != null) {\n          // 无损\n          audiosList.insert(0, data.dash!.flac!.audio!);\n        }\n\n        if (audiosList.isNotEmpty) {\n          final List<int> numbers = audiosList.map((map) => map.id!).toList();\n          int closestNumber = Utils.findClosestNumber(defaultAudioQa, numbers);\n          if (!numbers.contains(defaultAudioQa) &&\n              numbers.any((e) => e > defaultAudioQa)) {\n            closestNumber = 30280;\n          }\n          firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);\n        } else {\n          firstAudio = AudioItem();\n        }\n      } catch (err) {\n        firstAudio = audiosList.isNotEmpty ? audiosList.first : AudioItem();\n        SmartDialog.showToast('firstAudio error: $err');\n      }\n\n      audioUrl = enableCDN\n          ? VideoUtils.getCdnUrl(firstAudio)\n          : (firstAudio.backupUrl ?? firstAudio.baseUrl!);\n      //\n      if (firstAudio.id != null) {\n        currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;\n      }\n      defaultST = Duration(milliseconds: data.lastPlayTime!);\n      if (autoPlay.value) {\n        await playerInit();\n        isShowCover.value = false;\n      }\n    } else {\n      if (result['code'] == -404) {\n        isShowCover.value = false;\n      }\n      SmartDialog.showToast(result['msg'].toString());\n    }\n    return result;\n  }\n\n  // mob端全屏状态关闭二级回复\n  hiddenReplyReplyPanel() {\n    replyReplyBottomSheetCtr != null\n        ? replyReplyBottomSheetCtr!.close()\n        : print('replyReplyBottomSheetCtr is null');\n  }\n\n  // 获取字幕配置\n  Future getSubtitle() async {\n    var result = await VideoHttp.getSubtitle(bvid: bvid, cid: cid.value);\n    if (result['status']) {\n      if (result['data'].subtitles.isNotEmpty) {\n        subtitles = result['data'].subtitles;\n        getDanmaku(subtitles);\n      }\n    }\n  }\n\n  // 获取弹幕\n  Future getDanmaku(List subtitles) async {\n    if (subtitles.isNotEmpty) {\n      for (var i in subtitles) {\n        final Map<String, dynamic> res = await VideoHttp.getSubtitleContent(\n          i.subtitleUrl,\n        );\n        i.content = res['content'];\n        i.body = res['body'];\n      }\n    }\n  }\n\n  setSubtitleContent() {\n    plPlayerController.subtitleContent.value = '';\n    plPlayerController.subtitles.value = subtitles;\n  }\n\n  clearSubtitleContent() {\n    plPlayerController.subtitleContent.value = '';\n    plPlayerController.subtitles.value = [];\n  }\n\n  /// 发送弹幕\n  void showShootDanmakuSheet() {\n    final TextEditingController textController = TextEditingController();\n    bool isSending = false; // 追踪是否正在发送\n    showDialog(\n      context: Get.context!,\n      builder: (BuildContext context) {\n        // TODO: 支持更多类型和颜色的弹幕\n        return AlertDialog(\n          title: const Text('发送弹幕'),\n          content: StatefulBuilder(\n              builder: (BuildContext context, StateSetter setState) {\n            return TextField(\n              controller: textController,\n            );\n          }),\n          actions: [\n            TextButton(\n              onPressed: () => Get.back(),\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            StatefulBuilder(\n                builder: (BuildContext context, StateSetter setState) {\n              return TextButton(\n                onPressed: isSending\n                    ? null\n                    : () async {\n                        final String msg = textController.text;\n                        if (msg.isEmpty) {\n                          SmartDialog.showToast('弹幕内容不能为空');\n                          return;\n                        } else if (msg.length > 100) {\n                          SmartDialog.showToast('弹幕内容不能超过100个字符');\n                          return;\n                        }\n                        setState(() {\n                          isSending = true; // 开始发送，更新状态\n                        });\n                        //修改按钮文字\n                        // SmartDialog.showToast('弹幕发送中,\\n$msg');\n                        final dynamic res = await DanmakaHttp.shootDanmaku(\n                          oid: cid.value,\n                          msg: textController.text,\n                          bvid: bvid,\n                          progress:\n                              plPlayerController.position.value.inMilliseconds,\n                          type: 1,\n                        );\n                        setState(() {\n                          isSending = false; // 发送结束，更新状态\n                        });\n                        if (res['status']) {\n                          SmartDialog.showToast('发送成功');\n                          // 发送成功，自动预览该弹幕，避免重新请求\n                          // TODO: 暂停状态下预览弹幕仍会移动与计时，可考虑添加到dmSegList或其他方式实现\n                          plPlayerController.danmakuController?.addItems([\n                            DanmakuItem(\n                              msg,\n                              color: Colors.white,\n                              time: plPlayerController\n                                  .position.value.inMilliseconds,\n                              type: DanmakuItemType.scroll,\n                              isSend: true,\n                            )\n                          ]);\n                          Get.back();\n                        } else {\n                          SmartDialog.showToast('发送失败，错误信息为${res['msg']}');\n                        }\n                      },\n                child: Text(isSending ? '发送中...' : '发送'),\n              );\n            })\n          ],\n        );\n      },\n    );\n  }\n\n  void updateCover(String? pic) {\n    if (pic != null) {\n      cover.value = videoItem['pic'] = pic;\n    }\n  }\n\n  void onControllerCreated(ScrollController controller) {\n    replyScrollController = controller;\n  }\n\n  void onTapTabbar(int index) {\n    if (tabCtr.animation!.isCompleted && index == 1 && tabCtr.index == 1) {\n      replyScrollController?.animateTo(0,\n          duration: const Duration(milliseconds: 300), curve: Curves.ease);\n    }\n  }\n\n  void toggeleWatchLaterVisible(bool val) {\n    if (sourceType.value == 'watchLater' || sourceType.value == 'fav') {\n      isWatchLaterVisible.value = !isWatchLaterVisible.value;\n    }\n  }\n\n  // 获取稍后再看列表\n  Future fetchMediaList() async {\n    final Map argMap = Get.arguments;\n    var count = argMap['count'];\n    var res = await UserHttp.getMediaList(\n      type: 2,\n      bizId: userInfo.mid,\n      ps: count,\n    );\n    if (res['status']) {\n      mediaList = res['data'].reversed.toList();\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  // 稍后再看面板展开\n  showMediaListPanel() {\n    replyReplyBottomSheetCtr =\n        scaffoldKey.currentState?.showBottomSheet((BuildContext context) {\n      return MediaListPanel(\n        sheetHeight: sheetHeight.value,\n        mediaList: mediaList,\n        changeMediaList: changeMediaList,\n        panelTitle: watchLaterTitle.value,\n        bvid: bvid,\n        mediaId: Get.arguments['mediaId'],\n        hasMore: mediaList.length != Get.arguments['count'],\n      );\n    });\n    replyReplyBottomSheetCtr?.closed.then((value) {\n      isWatchLaterVisible.value = true;\n    });\n  }\n\n  // 切换稍后再看\n  Future changeMediaList(bvidVal, cidVal, aidVal, coverVal) async {\n    final VideoIntroController videoIntroCtr =\n        Get.find<VideoIntroController>(tag: heroTag);\n    bvid = bvidVal;\n    oid.value = aidVal ?? IdUtils.bv2av(bvid);\n    cid.value = cidVal;\n    danmakuCid.value = cidVal;\n    cover.value = coverVal;\n    queryVideoUrl();\n    clearSubtitleContent();\n    await getSubtitle();\n    setSubtitleContent();\n    // 重新请求评论\n    try {\n      /// 未渲染回复组件时可能异常\n      final VideoReplyController videoReplyCtr =\n          Get.find<VideoReplyController>(tag: heroTag);\n      videoReplyCtr.aid = aidVal;\n      videoReplyCtr.queryReplyList(type: 'init');\n    } catch (_) {}\n    videoIntroCtr.lastPlayCid.value = cidVal;\n    videoIntroCtr.bvid = bvidVal;\n    replyReplyBottomSheetCtr!.close();\n    await videoIntroCtr.queryVideoIntro();\n  }\n\n  // 获取收藏夹视频列表\n  Future queryFavVideoList() async {\n    final Map argMap = Get.arguments;\n    var mediaId = argMap['mediaId'];\n    var oid = argMap['oid'];\n    var res = await UserHttp.parseFavVideo(\n      mediaId: mediaId,\n      oid: oid,\n      bvid: bvid,\n    );\n    if (res['status']) {\n      mediaList = res['data'];\n    }\n  }\n\n  // 监听tabBarView切换\n  void onTabChanged() {\n    isWatchLaterVisible.value = tabCtr.index == 0;\n  }\n\n  @override\n  void onClose() {\n    super.onClose();\n    plPlayerController.dispose();\n    tabCtr.removeListener(() {\n      onTabChanged();\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/index.dart",
    "content": "library video_detail;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/controller.dart",
    "content": "import 'dart:async';\n\nimport 'package:bottom_sheet/bottom_sheet.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/constants.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/user/fav_folder.dart';\nimport 'package:pilipala/models/video/ai.dart';\nimport 'package:pilipala/models/video_detail_res.dart';\nimport 'package:pilipala/pages/video/detail/controller.dart';\nimport 'package:pilipala/pages/video/detail/reply/index.dart';\nimport 'package:pilipala/plugin/pl_player/models/play_repeat.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:share_plus/share_plus.dart';\n\nimport '../../../../common/pages_bottom_sheet.dart';\nimport '../../../../models/common/video_episode_type.dart';\nimport '../../../../utils/drawer.dart';\nimport '../related/index.dart';\nimport 'widgets/group_panel.dart';\n\nclass VideoIntroController extends GetxController {\n  VideoIntroController({required this.bvid});\n  // 视频bvid\n  String bvid;\n  // 视频详情 请求返回\n  Rx<VideoDetailData> videoDetail = VideoDetailData().obs;\n  // up主粉丝数\n  RxInt follower = 0.obs;\n  // 是否点赞\n  RxBool hasLike = false.obs;\n  // 是否投币\n  RxBool hasCoin = false.obs;\n  // 是否收藏\n  RxBool hasFav = false.obs;\n  // 是否不喜欢\n  RxBool hasDisLike = false.obs;\n  Box userInfoCache = GStrorage.userInfo;\n  bool userLogin = false;\n  Rx<FavFolderData> favFolderData = FavFolderData().obs;\n  List addMediaIdsNew = [];\n  List delMediaIdsNew = [];\n  // 关注状态 默认未关注\n  RxMap followStatus = {}.obs;\n  int _tempThemeValue = -1;\n\n  RxInt lastPlayCid = 0.obs;\n  var userInfo;\n\n  // 同时观看\n  bool isShowOnlineTotal = false;\n  RxString total = '1'.obs;\n  Timer? timer;\n  bool isPaused = false;\n  String heroTag = '';\n  late ModelResult modelResult;\n  PersistentBottomSheetController? bottomSheetController;\n  late bool enableRelatedVideo;\n\n  @override\n  void onInit() {\n    super.onInit();\n    userInfo = userInfoCache.get('userInfoCache');\n    try {\n      heroTag = Get.arguments['heroTag'];\n    } catch (_) {}\n    userLogin = userInfo != null;\n    lastPlayCid.value = int.parse(Get.parameters['cid']!);\n    isShowOnlineTotal =\n        setting.get(SettingBoxKey.enableOnlineTotal, defaultValue: false);\n    if (isShowOnlineTotal) {\n      queryOnlineTotal();\n      startTimer(); // 在页面加载时启动定时器\n    }\n    enableRelatedVideo =\n        setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true);\n  }\n\n  // 获取视频简介&分p\n  Future queryVideoIntro() async {\n    var result = await VideoHttp.videoIntro(bvid: bvid);\n    if (result['status']) {\n      videoDetail.value = result['data']!;\n      if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {\n        lastPlayCid.value = videoDetail.value.pages!.first.cid!;\n      }\n      final VideoDetailController videoDetailCtr =\n          Get.find<VideoDetailController>(tag: heroTag);\n      videoDetailCtr.tabs.value = ['简介', '评论 ${result['data']?.stat?.reply}'];\n      videoDetailCtr.cover.value = result['data'].pic ?? '';\n      // 获取到粉丝数再返回\n      await queryUserStat();\n    }\n    if (userLogin) {\n      // 获取点赞状态\n      queryHasLikeVideo();\n      // 获取投币状态\n      queryHasCoinVideo();\n      // 获取收藏状态\n      queryHasFavVideo();\n      //\n      queryFollowStatus();\n    }\n    return result;\n  }\n\n  // 获取up主粉丝数\n  Future queryUserStat() async {\n    var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);\n    if (result['status']) {\n      follower.value = result['data']['follower'];\n    }\n  }\n\n  // 获取点赞状态\n  Future queryHasLikeVideo() async {\n    var result = await VideoHttp.hasLikeVideo(bvid: bvid);\n    // data\tnum\t被点赞标志\t0：未点赞  1：已点赞\n    hasLike.value = result[\"data\"] == 1 ? true : false;\n  }\n\n  // 获取投币状态\n  Future queryHasCoinVideo() async {\n    var result = await VideoHttp.hasCoinVideo(bvid: bvid);\n    if (result['status']) {\n      hasCoin.value = result[\"data\"]['multiply'] == 0 ? false : true;\n    }\n  }\n\n  // 获取收藏状态\n  Future queryHasFavVideo() async {\n    /// fix 延迟查询\n    await Future.delayed(const Duration(milliseconds: 200));\n    var result = await VideoHttp.hasFavVideo(aid: IdUtils.bv2av(bvid));\n    if (result['status']) {\n      hasFav.value = result[\"data\"]['favoured'];\n    } else {\n      hasFav.value = false;\n    }\n  }\n\n  // 一键三连\n  Future actionOneThree() async {\n    if (userInfo == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    if (hasLike.value && hasCoin.value && hasFav.value) {\n      // 已点赞、投币、收藏\n      SmartDialog.showToast('UP已经收到了～');\n      return false;\n    }\n    var result = await VideoHttp.oneThree(bvid: bvid);\n    if (result['status']) {\n      hasLike.value = result[\"data\"][\"like\"];\n      hasCoin.value = result[\"data\"][\"coin\"];\n      hasFav.value = result[\"data\"][\"fav\"];\n      SmartDialog.showToast('三连成功');\n    } else {\n      SmartDialog.showToast(result['msg']);\n    }\n  }\n\n  // （取消）点赞\n  Future actionLikeVideo() async {\n    if (userInfo == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);\n    if (result['status']) {\n      if (!hasLike.value) {\n        SmartDialog.showToast('点赞成功');\n        hasLike.value = true;\n        videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1;\n      } else if (hasLike.value) {\n        SmartDialog.showToast('取消赞');\n        hasLike.value = false;\n        videoDetail.value.stat!.like = videoDetail.value.stat!.like! - 1;\n      }\n      hasLike.refresh();\n    } else {\n      SmartDialog.showToast(result['msg']);\n    }\n  }\n\n  // 投币\n  Future actionCoinVideo() async {\n    if (userInfo == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    showDialog(\n        context: Get.context!,\n        builder: (context) {\n          return AlertDialog(\n            title: const Text('选择投币个数'),\n            contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),\n            content: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [1, 2]\n                  .map(\n                    (e) => ListTile(\n                      title: Padding(\n                        padding: const EdgeInsets.only(left: 20),\n                        child: Text('$e 枚'),\n                      ),\n                      onTap: () async {\n                        var res =\n                            await VideoHttp.coinVideo(bvid: bvid, multiply: e);\n                        if (res['status']) {\n                          SmartDialog.showToast('投币成功');\n                          hasCoin.value = true;\n                          videoDetail.value.stat!.coin =\n                              videoDetail.value.stat!.coin! + e;\n                        } else {\n                          SmartDialog.showToast(res['msg']);\n                        }\n                        Get.back();\n                      },\n                    ),\n                  )\n                  .toList(),\n            ),\n          );\n        });\n  }\n\n  // （取消）收藏\n  Future actionFavVideo({type = 'choose'}) async {\n    // 收藏至默认文件夹\n    if (type == 'default') {\n      await queryVideoInFolder();\n      int defaultFolderId = favFolderData.value.list!.first.id!;\n      int favStatus = favFolderData.value.list!.first.favState!;\n      var result = await VideoHttp.favVideo(\n        aid: IdUtils.bv2av(bvid),\n        addIds: favStatus == 0 ? '$defaultFolderId' : '',\n        delIds: favStatus == 1 ? '$defaultFolderId' : '',\n      );\n      if (result['status']) {\n        // 重新获取收藏状态\n        await queryHasFavVideo();\n        SmartDialog.showToast('操作成功');\n      } else {\n        SmartDialog.showToast(result['msg']);\n      }\n      return;\n    }\n    try {\n      for (var i in favFolderData.value.list!.toList()) {\n        if (i.favState == 1) {\n          addMediaIdsNew.add(i.id);\n        } else {\n          delMediaIdsNew.add(i.id);\n        }\n      }\n    } catch (e) {\n      // ignore: avoid_print\n      print(e);\n    }\n    SmartDialog.showLoading(msg: '请求中');\n    var result = await VideoHttp.favVideo(\n        aid: IdUtils.bv2av(bvid),\n        addIds: addMediaIdsNew.join(','),\n        delIds: delMediaIdsNew.join(','));\n    SmartDialog.dismiss();\n    if (result['status']) {\n      addMediaIdsNew = [];\n      delMediaIdsNew = [];\n      Get.back();\n      // 重新获取收藏状态\n      await queryHasFavVideo();\n      SmartDialog.showToast('操作成功');\n    } else {\n      SmartDialog.showToast(result['msg']);\n    }\n  }\n\n  // 分享视频\n  Future actionShareVideo() async {\n    var result = await Share.share(\n            '${videoDetail.value.title} UP主: ${videoDetail.value.owner!.name!} - ${HttpString.baseUrl}/video/$bvid')\n        .whenComplete(() {});\n    return result;\n  }\n\n  Future queryVideoInFolder() async {\n    var result = await VideoHttp.videoInFolder(\n        mid: userInfo.mid, rid: IdUtils.bv2av(bvid));\n    if (result['status']) {\n      favFolderData.value = result['data'];\n    }\n    return result;\n  }\n\n  // 选择文件夹\n  onChoose(bool checkValue, int index) {\n    feedBack();\n    List<FavFolderItemData> datalist = favFolderData.value.list!;\n    for (var i = 0; i < datalist.length; i++) {\n      if (i == index) {\n        datalist[i].favState = checkValue == true ? 1 : 0;\n        datalist[i].mediaCount = checkValue == true\n            ? datalist[i].mediaCount! + 1\n            : datalist[i].mediaCount! - 1;\n      }\n    }\n    favFolderData.value.list = datalist;\n    favFolderData.refresh();\n  }\n\n  // 查询关注状态\n  Future queryFollowStatus() async {\n    if (videoDetail.value.owner == null) {\n      return;\n    }\n    var result = await VideoHttp.hasFollow(mid: videoDetail.value.owner!.mid!);\n    if (result['status']) {\n      followStatus.value = result['data'];\n    }\n    return result;\n  }\n\n  // 关注/取关up\n  Future actionRelationMod() async {\n    feedBack();\n    if (userInfo == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    final int currentStatus = followStatus['attribute'];\n    int actionStatus = 0;\n    switch (currentStatus) {\n      case 0:\n        actionStatus = 1;\n        break;\n      case 2:\n        actionStatus = 2;\n        break;\n      default:\n        actionStatus = 0;\n        break;\n    }\n    SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: Text(currentStatus == 0 ? '关注UP主?' : '取消关注UP主?'),\n          actions: [\n            TextButton(\n              onPressed: () => SmartDialog.dismiss(),\n              child: Text(\n                '点错了',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                var result = await VideoHttp.relationMod(\n                  mid: videoDetail.value.owner!.mid!,\n                  act: actionStatus,\n                  reSrc: 14,\n                );\n                if (result['status']) {\n                  switch (currentStatus) {\n                    case 0:\n                      actionStatus = 2;\n                      break;\n                    case 2:\n                      actionStatus = 0;\n                      break;\n                    default:\n                      actionStatus = 0;\n                      break;\n                  }\n                  followStatus['attribute'] = actionStatus;\n                  followStatus.refresh();\n                  if (actionStatus == 2) {\n                    if (context.mounted) {\n                      ScaffoldMessenger.of(context).showSnackBar(\n                        SnackBar(\n                          content: const Text('关注成功'),\n                          duration: const Duration(seconds: 2),\n                          action: SnackBarAction(\n                            label: '设置分组',\n                            onPressed: setFollowGroup,\n                          ),\n                          showCloseIcon: true,\n                        ),\n                      );\n                    }\n                  }\n                }\n                SmartDialog.dismiss();\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  // 修改分P或番剧分集\n  Future changeSeasonOrbangu(\n    String bvid,\n    int cid,\n    int? aid,\n    String? cover,\n  ) async {\n    // 重新获取视频资源\n    final VideoDetailController videoDetailCtr =\n        Get.find<VideoDetailController>(tag: heroTag);\n    if (enableRelatedVideo) {\n      final ReleatedController releatedCtr =\n          Get.find<ReleatedController>(tag: heroTag);\n      releatedCtr.bvid = bvid;\n      releatedCtr.queryRelatedVideo();\n    }\n\n    videoDetailCtr\n      ..bvid = bvid\n      ..oid.value = aid ?? IdUtils.bv2av(bvid)\n      ..cid.value = cid\n      ..danmakuCid.value = cid\n      ..cover.value = cover ?? ''\n      ..queryVideoUrl()\n      ..clearSubtitleContent();\n    await videoDetailCtr.getSubtitle();\n    videoDetailCtr.setSubtitleContent();\n    // 重新请求评论\n    try {\n      /// 未渲染回复组件时可能异常\n      final VideoReplyController videoReplyCtr =\n          Get.find<VideoReplyController>(tag: heroTag);\n      videoReplyCtr.aid = aid;\n      videoReplyCtr.queryReplyList(type: 'init');\n    } catch (_) {}\n    this.bvid = bvid;\n    lastPlayCid.value = cid;\n    await queryVideoIntro();\n  }\n\n  void startTimer() {\n    const duration = Duration(seconds: 10); // 设置定时器间隔为10秒\n    timer = Timer.periodic(duration, (Timer timer) {\n      if (!isPaused) {\n        queryOnlineTotal(); // 定时器回调函数，发起请求\n      }\n    });\n  }\n\n  // 查看同时在看人数\n  Future queryOnlineTotal() async {\n    var result = await VideoHttp.onlineTotal(\n      aid: IdUtils.bv2av(bvid),\n      bvid: bvid,\n      cid: lastPlayCid.value,\n    );\n    if (result['status']) {\n      total.value = result['data']['total'];\n    }\n  }\n\n  @override\n  void onClose() {\n    if (timer != null) {\n      timer!.cancel(); // 销毁页面时取消定时器\n    }\n    super.onClose();\n  }\n\n  /// 列表循环或者顺序播放时，自动播放下一个\n  void nextPlay() {\n    final List episodes = [];\n    bool isPages = false;\n    late String cover;\n    final VideoDetailController videoDetailCtr =\n        Get.find<VideoDetailController>(tag: heroTag);\n\n    /// 优先稍后再看、收藏夹\n    if (videoDetailCtr.isWatchLaterVisible.value) {\n      episodes.addAll(videoDetailCtr.mediaList);\n    } else if (videoDetail.value.ugcSeason != null) {\n      final UgcSeason ugcSeason = videoDetail.value.ugcSeason!;\n      final List<SectionItem> sections = ugcSeason.sections!;\n      for (int i = 0; i < sections.length; i++) {\n        final List<EpisodeItem> episodesList = sections[i].episodes!;\n        episodes.addAll(episodesList);\n      }\n    } else if (videoDetail.value.pages != null) {\n      isPages = true;\n      final List<Part> pages = videoDetail.value.pages!;\n      episodes.addAll(pages);\n    }\n\n    final int currentIndex =\n        episodes.indexWhere((e) => e.cid == lastPlayCid.value);\n    int nextIndex = currentIndex + 1;\n    cover = episodes[nextIndex].cover;\n    final PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;\n\n    int cid = episodes[nextIndex].cid!;\n    while (cid == -1) {\n      nextIndex += 1;\n      SmartDialog.showToast('当前视频暂不支持播放，自动跳过');\n      cid = episodes[nextIndex].cid!;\n    }\n\n    // 列表循环\n    if (nextIndex >= episodes.length) {\n      if (platRepeat == PlayRepeat.listCycle) {\n        nextIndex = 0;\n      }\n      if (platRepeat == PlayRepeat.listOrder) {\n        return;\n      }\n    }\n    final String rBvid = isPages ? bvid : episodes[nextIndex].bvid;\n    final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;\n    changeSeasonOrbangu(rBvid, cid, rAid, cover);\n  }\n\n  // 设置关注分组\n  void setFollowGroup() {\n    showFlexibleBottomSheet(\n      bottomSheetBorderRadius: const BorderRadius.only(\n        topLeft: Radius.circular(16),\n        topRight: Radius.circular(16),\n      ),\n      minHeight: 0.6,\n      initHeight: 0.6,\n      maxHeight: 1,\n      context: Get.context!,\n      builder: (BuildContext context, ScrollController scrollController,\n          double offset) {\n        return GroupPanel(\n          mid: videoDetail.value.owner!.mid!,\n          scrollController: scrollController,\n        );\n      },\n      anchors: [0.6, 1],\n      isSafeArea: true,\n    );\n  }\n\n  // ai总结\n  Future aiConclusion() async {\n    SmartDialog.showLoading(msg: '正在生成ai总结');\n    final res = await VideoHttp.aiConclusion(\n      bvid: bvid,\n      cid: lastPlayCid.value,\n      upMid: videoDetail.value.owner!.mid!,\n    );\n    SmartDialog.dismiss();\n    if (res['status']) {\n      modelResult = res['data'].modelResult;\n    } else {\n      SmartDialog.showToast(\"当前视频暂不支持AI视频总结\");\n    }\n    return res;\n  }\n\n  hiddenEpisodeBottomSheet() {\n    bottomSheetController?.close();\n  }\n\n  // 播放器底栏 选集 回调\n  void showEposideHandler() {\n    late List episodes;\n    VideoEpidoesType dataType = VideoEpidoesType.videoEpisode;\n    if (videoDetail.value.ugcSeason != null) {\n      dataType = VideoEpidoesType.videoEpisode;\n      final List<SectionItem> sections = videoDetail.value.ugcSeason!.sections!;\n      for (int i = 0; i < sections.length; i++) {\n        final List<EpisodeItem> episodesList = sections[i].episodes!;\n        for (int j = 0; j < episodesList.length; j++) {\n          if (episodesList[j].cid == lastPlayCid.value) {\n            episodes = episodesList;\n            continue;\n          }\n        }\n      }\n    }\n    if (videoDetail.value.pages != null &&\n        videoDetail.value.pages!.length > 1) {\n      dataType = VideoEpidoesType.videoPart;\n      episodes = videoDetail.value.pages!;\n    }\n\n    DrawerUtils.showRightDialog(\n      child: EpisodeBottomSheet(\n        episodes: episodes,\n        currentCid: lastPlayCid.value,\n        dataType: dataType,\n        context: Get.context!,\n        sheetHeight: Get.size.height,\n        isFullScreen: true,\n        changeFucCall: (item, index) {\n          if (dataType == VideoEpidoesType.videoEpisode) {\n            changeSeasonOrbangu(\n                IdUtils.av2bv(item.aid), item.cid, item.aid, item.cover);\n          }\n          if (dataType == VideoEpidoesType.videoPart) {\n            changeSeasonOrbangu(bvid, item.cid, null, item.cover);\n          }\n          SmartDialog.dismiss();\n        },\n      ).buildShowContent(Get.context!),\n    );\n  }\n\n  //\n  oneThreeDialog() {\n    showDialog(\n      context: Get.context!,\n      builder: (context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: const Text('是否一键三连'),\n          actions: [\n            TextButton(\n              onPressed: () => navigator!.pop(),\n              child: Text(\n                '取消',\n                style: TextStyle(\n                    color: Theme.of(Get.context!).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                actionOneThree();\n                navigator!.pop();\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/index.dart",
    "content": "library video_intro_panel;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/view.dart",
    "content": "import 'package:bottom_sheet/bottom_sheet.dart';\nimport 'package:expandable/expandable.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hive/hive.dart';\nimport 'package:lottie/lottie.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/stat/danmu.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/models/video_detail_res.dart';\nimport 'package:pilipala/pages/video/detail/introduction/controller.dart';\nimport 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport '../../../../http/user.dart';\nimport 'widgets/action_item.dart';\nimport 'widgets/fav_panel.dart';\nimport 'widgets/intro_detail.dart';\nimport 'widgets/page_panel.dart';\nimport 'widgets/season_panel.dart';\nimport 'widgets/staff_up_item.dart';\n\nclass VideoIntroPanel extends StatefulWidget {\n  final String bvid;\n  final String? cid;\n\n  const VideoIntroPanel({super.key, required this.bvid, this.cid});\n\n  @override\n  State<VideoIntroPanel> createState() => _VideoIntroPanelState();\n}\n\nclass _VideoIntroPanelState extends State<VideoIntroPanel>\n    with AutomaticKeepAliveClientMixin {\n  late String heroTag;\n  late VideoIntroController videoIntroController;\n  VideoDetailData? videoDetail;\n  late Future? _futureBuilderFuture;\n\n  // 添加页面缓存\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n\n    /// fix 全屏时参数丢失\n    heroTag = Get.arguments['heroTag'];\n    videoIntroController =\n        Get.put(VideoIntroController(bvid: widget.bvid), tag: heroTag);\n    _futureBuilderFuture = videoIntroController.queryVideoIntro();\n    videoIntroController.videoDetail.listen((value) {\n      videoDetail = value;\n    });\n  }\n\n  @override\n  void dispose() {\n    videoIntroController.onClose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return FutureBuilder(\n      future: _futureBuilderFuture,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == ConnectionState.done) {\n          if (snapshot.data == null) {\n            return const SliverToBoxAdapter(child: SizedBox());\n          }\n          if (snapshot.data['status']) {\n            // 请求成功\n            return Obx(\n              () => VideoInfo(\n                videoDetail: videoIntroController.videoDetail.value,\n                heroTag: heroTag,\n                bvid: widget.bvid,\n              ),\n            );\n          } else {\n            // 请求错误\n            return HttpError(\n              errMsg: snapshot.data['msg'],\n              btnText: snapshot.data['code'] == -404 ||\n                      snapshot.data['code'] == 62002\n                  ? '返回上一页'\n                  : null,\n              fn: () => Get.back(),\n            );\n          }\n        } else {\n          return SliverToBoxAdapter(\n            child: SizedBox(\n              height: 100,\n              child: Center(\n                child: Lottie.asset(\n                  'assets/loading.json',\n                  width: 200,\n                ),\n              ),\n            ),\n          );\n        }\n      },\n    );\n  }\n}\n\nclass VideoInfo extends StatefulWidget {\n  final VideoDetailData? videoDetail;\n  final String? heroTag;\n  final String bvid;\n\n  const VideoInfo({\n    Key? key,\n    this.videoDetail,\n    this.heroTag,\n    required this.bvid,\n  }) : super(key: key);\n\n  @override\n  State<VideoInfo> createState() => _VideoInfoState();\n}\n\nclass _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {\n  late String heroTag;\n  late final VideoIntroController videoIntroController;\n  late final VideoDetailController videoDetailCtr;\n  final Box<dynamic> localCache = GStrorage.localCache;\n  final Box<dynamic> setting = GStrorage.setting;\n  late double sheetHeight;\n  late final dynamic owner;\n  late int mid;\n  late String memberHeroTag;\n  late bool enableAi;\n  bool isProcessing = false;\n  RxBool isExpand = false.obs;\n  late ExpandableController _expandableCtr;\n\n  void Function()? handleState(Future<dynamic> Function() action) {\n    return isProcessing\n        ? null\n        : () async {\n            isProcessing = true;\n            await action.call();\n            isProcessing = false;\n          };\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    heroTag = widget.heroTag!;\n    videoIntroController =\n        Get.put(VideoIntroController(bvid: widget.bvid), tag: heroTag);\n    videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);\n    sheetHeight = localCache.get('sheetHeight');\n\n    owner = widget.videoDetail!.owner;\n    enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);\n    _expandableCtr = ExpandableController(initialExpanded: false);\n  }\n\n  // 收藏\n  showFavBottomSheet({type = 'tap'}) {\n    if (videoIntroController.userInfo == null) {\n      SmartDialog.showToast('账号未登录');\n      return;\n    }\n    final bool enableDragQuickFav =\n        setting.get(SettingBoxKey.enableQuickFav, defaultValue: false);\n    // 快速收藏 &\n    // 点按 收藏至默认文件夹\n    // 长按选择文件夹\n    if (enableDragQuickFav) {\n      if (type == 'tap') {\n        if (!videoIntroController.hasFav.value) {\n          videoIntroController.actionFavVideo(type: 'default');\n        } else {\n          _showFavPanel();\n        }\n      } else {\n        _showFavPanel();\n      }\n    } else if (type != 'longPress') {\n      _showFavPanel();\n    }\n  }\n\n  void _showFavPanel() {\n    showFlexibleBottomSheet(\n      bottomSheetBorderRadius: const BorderRadius.only(\n        topLeft: Radius.circular(16),\n        topRight: Radius.circular(16),\n      ),\n      minHeight: 0.6,\n      initHeight: 0.6,\n      maxHeight: 1,\n      context: context,\n      builder: (BuildContext context, ScrollController scrollController,\n          double offset) {\n        return FavPanel(\n          ctr: videoIntroController,\n          scrollController: scrollController,\n        );\n      },\n      anchors: [0.6, 1],\n      isSafeArea: true,\n    );\n  }\n\n  // 视频介绍\n  showIntroDetail() {\n    feedBack();\n    isExpand.value = !(isExpand.value);\n    _expandableCtr.toggle();\n  }\n\n  // 用户主页\n  onPushMember() {\n    feedBack();\n    mid = widget.videoDetail!.owner!.mid!;\n    memberHeroTag = Utils.makeHeroTag(mid);\n    String face = widget.videoDetail!.owner!.face!;\n    Get.toNamed('/member?mid=$mid',\n        arguments: {'face': face, 'heroTag': memberHeroTag});\n  }\n\n  // ai总结\n  showAiBottomSheet() {\n    showBottomSheet(\n      context: context,\n      enableDrag: true,\n      builder: (BuildContext context) {\n        return AiDetail(modelResult: videoIntroController.modelResult);\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _expandableCtr.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final ThemeData t = Theme.of(context);\n    final Color outline = t.colorScheme.outline;\n    return SliverPadding(\n      padding: const EdgeInsets.only(\n        left: StyleString.safeSpace,\n        right: StyleString.safeSpace,\n        top: 16,\n      ),\n      sliver: SliverToBoxAdapter(\n          child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onTap: () => showIntroDetail(),\n            onLongPress: () async {\n              feedBack();\n              await Clipboard.setData(\n                  ClipboardData(text: widget.videoDetail!.title!));\n              SmartDialog.showToast('标题已复制');\n            },\n            child: ExpandablePanel(\n              controller: _expandableCtr,\n              collapsed: Text(\n                widget.videoDetail!.title!,\n                softWrap: true,\n                maxLines: 2,\n                overflow: TextOverflow.ellipsis,\n                style: const TextStyle(\n                  fontSize: 18,\n                  fontWeight: FontWeight.bold,\n                ),\n              ),\n              expanded: Text(\n                widget.videoDetail!.title!,\n                softWrap: true,\n                maxLines: 10,\n                style: const TextStyle(\n                  fontSize: 18,\n                  fontWeight: FontWeight.bold,\n                ),\n              ),\n              theme: const ExpandableThemeData(\n                animationDuration: Duration(milliseconds: 300),\n                scrollAnimationDuration: Duration(milliseconds: 300),\n                crossFadePoint: 0,\n                fadeCurve: Curves.ease,\n                sizeCurve: Curves.linear,\n              ),\n            ),\n          ),\n          Stack(\n            children: [\n              GestureDetector(\n                behavior: HitTestBehavior.translucent,\n                onTap: () => showIntroDetail(),\n                child: Padding(\n                  padding: const EdgeInsets.only(top: 7, bottom: 6),\n                  child: Row(\n                    children: [\n                      StatView(\n                        view: widget.videoDetail!.stat!.view,\n                        size: 'medium',\n                      ),\n                      const SizedBox(width: 10),\n                      StatDanMu(\n                        danmu: widget.videoDetail!.stat!.danmaku,\n                        size: 'medium',\n                      ),\n                      const SizedBox(width: 10),\n                      Text(\n                        Utils.dateFormat(widget.videoDetail!.pubdate,\n                            formatType: 'detail'),\n                        style: TextStyle(\n                          fontSize: 12,\n                          color: t.colorScheme.outline,\n                        ),\n                      ),\n                      const SizedBox(width: 10),\n                      if (videoIntroController.isShowOnlineTotal)\n                        Obx(\n                          () => Text(\n                            '${videoIntroController.total.value}人在看',\n                            style: TextStyle(\n                              fontSize: 12,\n                              color: t.colorScheme.outline,\n                            ),\n                          ),\n                        ),\n                    ],\n                  ),\n                ),\n              ),\n              if (enableAi)\n                Positioned(\n                  right: 10,\n                  top: 6,\n                  child: GestureDetector(\n                    onTap: () async {\n                      final res = await videoIntroController.aiConclusion();\n                      if (res['status']) {\n                        showAiBottomSheet();\n                      }\n                    },\n                    child: Image.asset('assets/images/ai.png', height: 22),\n                  ),\n                )\n            ],\n          ),\n\n          /// 视频简介\n          ExpandablePanel(\n            controller: _expandableCtr,\n            collapsed: const SizedBox(height: 0),\n            expanded: IntroDetail(videoDetail: widget.videoDetail!),\n            theme: const ExpandableThemeData(\n              animationDuration: Duration(milliseconds: 300),\n              scrollAnimationDuration: Duration(milliseconds: 300),\n              crossFadePoint: 0,\n              fadeCurve: Curves.ease,\n              sizeCurve: Curves.linear,\n            ),\n          ),\n\n          /// 点赞收藏转发\n          Material(child: actionGrid(context, videoIntroController)),\n          // 合集 videoPart 简洁\n          if (widget.videoDetail!.ugcSeason != null) ...[\n            Obx(\n              () => SeasonPanel(\n                ugcSeason: widget.videoDetail!.ugcSeason!,\n                cid: videoIntroController.lastPlayCid.value != 0\n                    ? videoIntroController.lastPlayCid.value\n                    : widget.videoDetail!.pages!.first.cid,\n                sheetHeight: videoDetailCtr.sheetHeight.value,\n                changeFuc: (bvid, cid, aid, cover) =>\n                    videoIntroController.changeSeasonOrbangu(\n                  bvid,\n                  cid,\n                  aid,\n                  cover,\n                ),\n                videoIntroCtr: videoIntroController,\n              ),\n            )\n          ],\n          // 合集 videoEpisode\n          if (widget.videoDetail!.pages != null &&\n              widget.videoDetail!.pages!.length > 1) ...[\n            Obx(\n              () => PagesPanel(\n                pages: widget.videoDetail!.pages!,\n                cid: videoIntroController.lastPlayCid.value,\n                sheetHeight: videoDetailCtr.sheetHeight.value,\n                changeFuc: (cid, cover) =>\n                    videoIntroController.changeSeasonOrbangu(\n                  videoIntroController.bvid,\n                  cid,\n                  null,\n                  cover,\n                ),\n                videoIntroCtr: videoIntroController,\n              ),\n            )\n          ],\n          if (widget.videoDetail!.staff == null)\n            GestureDetector(\n              onTap: onPushMember,\n              child: Container(\n                padding:\n                    const EdgeInsets.symmetric(vertical: 12, horizontal: 4),\n                child: Row(\n                  children: [\n                    NetworkImgLayer(\n                      type: 'avatar',\n                      src: widget.videoDetail!.owner!.face,\n                      width: 34,\n                      height: 34,\n                      fadeInDuration: Duration.zero,\n                      fadeOutDuration: Duration.zero,\n                    ),\n                    const SizedBox(width: 10),\n                    Text(widget.videoDetail!.owner!.name!,\n                        style: const TextStyle(fontSize: 13)),\n                    const SizedBox(width: 6),\n                    Obx(\n                      () => Text(\n                        Utils.numFormat(videoIntroController.follower.value),\n                        style: TextStyle(\n                          fontSize: t.textTheme.labelSmall!.fontSize,\n                          color: outline,\n                        ),\n                      ),\n                    ),\n                    const Spacer(),\n                    Obx(\n                      () {\n                        final bool isFollowed =\n                            videoIntroController.followStatus['attribute'] != 0;\n                        return videoIntroController.followStatus.isEmpty\n                            ? const SizedBox()\n                            : SizedBox(\n                                height: 32,\n                                child: TextButton(\n                                  onPressed:\n                                      videoIntroController.actionRelationMod,\n                                  style: TextButton.styleFrom(\n                                    padding: const EdgeInsets.only(\n                                      left: 8,\n                                      right: 8,\n                                    ),\n                                    foregroundColor: isFollowed\n                                        ? outline\n                                        : t.colorScheme.onPrimary,\n                                    backgroundColor: isFollowed\n                                        ? t.colorScheme.onInverseSurface\n                                        : t.colorScheme.primary, // 设置按钮背景色\n                                  ),\n                                  child: Text(\n                                    isFollowed ? '已关注' : '关注',\n                                    style: TextStyle(\n                                      fontSize:\n                                          t.textTheme.labelMedium!.fontSize,\n                                    ),\n                                  ),\n                                ),\n                              );\n                      },\n                    )\n                  ],\n                ),\n              ),\n            ),\n          if (widget.videoDetail!.staff != null) ...[\n            const SizedBox(height: 15),\n            Column(\n              mainAxisAlignment: MainAxisAlignment.start,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text.rich(\n                  TextSpan(\n                    style: TextStyle(\n                      fontSize:\n                          Theme.of(context).textTheme.labelMedium!.fontSize,\n                    ),\n                    children: [\n                      TextSpan(\n                        text: '创作团队',\n                        style: Theme.of(context)\n                            .textTheme\n                            .titleSmall!\n                            .copyWith(fontWeight: FontWeight.bold),\n                      ),\n                      const WidgetSpan(child: SizedBox(width: 6)),\n                      TextSpan(\n                        text: '${widget.videoDetail!.staff!.length}人',\n                        style: TextStyle(\n                          color: Theme.of(context).colorScheme.outline,\n                        ),\n                      )\n                    ],\n                  ),\n                ),\n                SizedBox(\n                  height: 120,\n                  child: ListView(\n                    scrollDirection: Axis.horizontal,\n                    children: [\n                      for (int i = 0;\n                          i < widget.videoDetail!.staff!.length;\n                          i++) ...[\n                        StaffUpItem(item: widget.videoDetail!.staff![i])\n                      ],\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ]\n        ],\n      )),\n    );\n  }\n\n  Widget actionGrid(BuildContext context, videoIntroController) {\n    final actionTypeSort = GlobalDataCache().actionTypeSort;\n\n    Map<String, Widget> menuListWidgets = {\n      'like': Obx(\n        () => ActionItem(\n          icon: const Icon(FontAwesomeIcons.thumbsUp),\n          selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),\n          onTap: handleState(videoIntroController.actionLikeVideo),\n          onLongPress: () => videoIntroController.oneThreeDialog(),\n          selectStatus: videoIntroController.hasLike.value,\n          text: widget.videoDetail!.stat!.like!.toString(),\n        ),\n      ),\n      'coin': Obx(\n        () => ActionItem(\n          icon: const Icon(FontAwesomeIcons.b),\n          selectIcon: const Icon(FontAwesomeIcons.b),\n          onTap: handleState(videoIntroController.actionCoinVideo),\n          selectStatus: videoIntroController.hasCoin.value,\n          text: widget.videoDetail!.stat!.coin!.toString(),\n        ),\n      ),\n      'collect': Obx(\n        () => ActionItem(\n          icon: const Icon(FontAwesomeIcons.star),\n          selectIcon: const Icon(FontAwesomeIcons.solidStar),\n          onTap: () => showFavBottomSheet(),\n          onLongPress: () => showFavBottomSheet(type: 'longPress'),\n          selectStatus: videoIntroController.hasFav.value,\n          text: widget.videoDetail!.stat!.favorite!.toString(),\n        ),\n      ),\n      'watchLater': ActionItem(\n        icon: const Icon(FontAwesomeIcons.clock),\n        onTap: () async {\n          final res =\n              await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid);\n          SmartDialog.showToast(res['msg']);\n        },\n        selectStatus: false,\n        text: '稍后看',\n      ),\n      'share': ActionItem(\n        icon: const Icon(FontAwesomeIcons.shareFromSquare),\n        onTap: () => videoIntroController.actionShareVideo(),\n        selectStatus: false,\n        text: '分享',\n      ),\n      'dislike': Obx(\n        () => ActionItem(\n          icon: const Icon(FontAwesomeIcons.thumbsDown),\n          selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown),\n          onTap: () {},\n          selectStatus: videoIntroController.hasDisLike.value,\n          text: '不喜欢',\n        ),\n      ),\n      'downloadCover': ActionItem(\n        icon: const Icon(Icons.image_outlined),\n        onTap: () {},\n        selectStatus: false,\n        text: '下载封面',\n      ),\n      'copyLink': ActionItem(\n        icon: const Icon(Icons.link_outlined),\n        onTap: () {},\n        selectStatus: false,\n        text: '复制链接',\n      ),\n    };\n    final List<Widget> list = [];\n    for (var i = 0; i < actionTypeSort.length; i++) {\n      list.add(menuListWidgets[actionTypeSort[i]]!);\n    }\n\n    return LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n      return Container(\n        margin: const EdgeInsets.only(top: 6, bottom: 4),\n        height: constraints.maxWidth / 5,\n        child: ListView(\n          scrollDirection: Axis.horizontal,\n          children: list,\n        ),\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/action_item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/utils/feed_back.dart';\n\nclass ActionItem extends StatelessWidget {\n  final dynamic icon;\n  final Icon? selectIcon;\n  final Function? onTap;\n  final Function? onLongPress;\n  final String? text;\n  final bool selectStatus;\n\n  const ActionItem({\n    Key? key,\n    this.icon,\n    this.selectIcon,\n    this.onTap,\n    this.onLongPress,\n    this.text,\n    this.selectStatus = false,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return InkWell(\n      onTap: () => {\n        feedBack(),\n        onTap!(),\n      },\n      onLongPress: () => {\n        if (onLongPress != null) {onLongPress!()}\n      },\n      borderRadius: StyleString.mdRadius,\n      child: SizedBox(\n        width: (Get.size.width - 24) / 5,\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const SizedBox(height: 4),\n            AnimatedSwitcher(\n              duration: const Duration(milliseconds: 300),\n              transitionBuilder: (Widget child, Animation<double> animation) {\n                return ScaleTransition(scale: animation, child: child);\n              },\n              child: icon is Icon\n                  ? Icon(\n                      selectStatus\n                          ? selectIcon!.icon ?? icon!.icon\n                          : icon!.icon,\n                      color: selectStatus\n                          ? Theme.of(context).colorScheme.primary\n                          : Theme.of(context).colorScheme.outline,\n                      size: 20,\n                    )\n                  : Image.asset(\n                      key: ValueKey<bool>(selectStatus),\n                      'assets/images/coin.png',\n                      width: const IconThemeData.fallback().size,\n                      color: selectStatus\n                          ? Theme.of(context).colorScheme.primary\n                          : Theme.of(context).colorScheme.outline,\n                    ),\n            ),\n            const SizedBox(height: 6),\n            Text(\n              text ?? '',\n              style: TextStyle(\n                color:\n                    selectStatus ? Theme.of(context).colorScheme.primary : null,\n                fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/action_row_item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/utils/feed_back.dart';\n\nclass ActionRowItem extends StatelessWidget {\n  final Icon? icon;\n  final Icon? selectIcon;\n  final Function? onTap;\n  final bool? loadingStatus;\n  final String? text;\n  final bool selectStatus;\n  final Function? onLongPress;\n\n  const ActionRowItem({\n    Key? key,\n    this.icon,\n    this.selectIcon,\n    this.onTap,\n    this.loadingStatus,\n    this.text,\n    this.selectStatus = false,\n    this.onLongPress,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: selectStatus\n          ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6)\n          : Theme.of(context).highlightColor.withOpacity(0.2),\n      borderRadius: const BorderRadius.all(Radius.circular(30)),\n      clipBehavior: Clip.hardEdge,\n      child: InkWell(\n        onTap: () => {\n          feedBack(),\n          onTap!(),\n        },\n        onLongPress: () {\n          feedBack();\n          if (onLongPress != null) {\n            onLongPress!();\n          }\n        },\n        child: Padding(\n          padding: const EdgeInsets.fromLTRB(15, 7, 15, 7),\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              if (icon != null) ...[\n                Icon(icon!.icon!,\n                    size: 13,\n                    color: selectStatus\n                        ? Theme.of(context).colorScheme.primary\n                        : Theme.of(context).colorScheme.onSecondaryContainer),\n                const SizedBox(width: 6),\n              ],\n              AnimatedOpacity(\n                opacity: loadingStatus! ? 0 : 1,\n                duration: const Duration(milliseconds: 200),\n                child: AnimatedSwitcher(\n                  duration: const Duration(milliseconds: 300),\n                  transitionBuilder:\n                      (Widget child, Animation<double> animation) {\n                    return ScaleTransition(scale: animation, child: child);\n                  },\n                  child: Text(\n                    text ?? '',\n                    key: ValueKey<String>(text ?? ''),\n                    style: TextStyle(\n                        color: selectStatus\n                            ? Theme.of(context).colorScheme.primary\n                            : null,\n                        fontSize:\n                            Theme.of(context).textTheme.labelMedium!.fontSize),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/fav_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass FavPanel extends StatefulWidget {\n  const FavPanel({super.key, this.ctr, this.scrollController});\n  final dynamic ctr;\n  final ScrollController? scrollController;\n\n  @override\n  State<FavPanel> createState() => _FavPanelState();\n}\n\nclass _FavPanelState extends State<FavPanel> {\n  final Box<dynamic> localCache = GStrorage.localCache;\n  late Future _futureBuilderFuture;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = widget.ctr!.queryVideoInFolder();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        AppBar(\n          centerTitle: false,\n          elevation: 0,\n          automaticallyImplyLeading: false,\n          leadingWidth: 0,\n          title: Text(\n            '选择收藏夹',\n            style: Theme.of(context)\n                .textTheme\n                .titleMedium!\n                .copyWith(fontWeight: FontWeight.bold),\n          ),\n        ),\n        Expanded(\n          child: Material(\n            child: FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (BuildContext context, AsyncSnapshot snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  Map data = snapshot.data as Map;\n                  if (data['status']) {\n                    return Obx(\n                      () => ListView.builder(\n                        controller: widget.scrollController,\n                        itemCount: widget.ctr!.favFolderData.value.list!.length,\n                        itemBuilder: (context, index) {\n                          final item =\n                              widget.ctr!.favFolderData.value.list![index];\n                          return ListTile(\n                            onTap: () =>\n                                widget.ctr!.onChoose(item.favState != 1, index),\n                            dense: true,\n                            leading: Icon([23, 1].contains(item.attr)\n                                ? Icons.lock_outline\n                                : Icons.folder_outlined),\n                            minLeadingWidth: 0,\n                            title: Text(item.title!),\n                            subtitle: Text(\n                              '${item.mediaCount}个内容 - ${[\n                                23,\n                                1\n                              ].contains(item.attr) ? '私密' : '公开'}',\n                            ),\n                            trailing: Transform.scale(\n                              scale: 0.9,\n                              child: Checkbox(\n                                value: item.favState == 1,\n                                onChanged: (bool? checkValue) =>\n                                    widget.ctr!.onChoose(checkValue!, index),\n                              ),\n                            ),\n                          );\n                        },\n                      ),\n                    );\n                  } else {\n                    return HttpError(\n                      errMsg: data['msg'],\n                      fn: () => setState(() {}),\n                    );\n                  }\n                } else {\n                  // 骨架屏\n                  return const Text('请求中');\n                }\n              },\n            ),\n          ),\n        ),\n        Divider(\n          height: 1,\n          color: Theme.of(context).disabledColor.withOpacity(0.08),\n        ),\n        Padding(\n          padding: EdgeInsets.only(\n            left: 20,\n            right: 20,\n            top: 12,\n            bottom: MediaQuery.of(context).padding.bottom + 12,\n          ),\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.end,\n            children: <Widget>[\n              TextButton(\n                onPressed: () => Get.back(),\n                style: TextButton.styleFrom(\n                  padding: const EdgeInsets.only(left: 30, right: 30),\n                  backgroundColor:\n                      Theme.of(context).colorScheme.onInverseSurface, // 设置按钮背景色\n                ),\n                child: const Text('取消'),\n              ),\n              const SizedBox(width: 10),\n              TextButton(\n                onPressed: () async {\n                  feedBack();\n                  await widget.ctr!.actionFavVideo();\n                },\n                style: TextButton.styleFrom(\n                  padding: const EdgeInsets.only(left: 30, right: 30),\n                  foregroundColor: Theme.of(context).colorScheme.onPrimary,\n                  backgroundColor:\n                      Theme.of(context).colorScheme.primary, // 设置按钮背景色\n                ),\n                child: const Text('确认'),\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/group_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/http/member.dart';\nimport 'package:pilipala/models/member/tags.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass GroupPanel extends StatefulWidget {\n  final int? mid;\n  final ScrollController scrollController;\n  const GroupPanel({super.key, this.mid, required this.scrollController});\n\n  @override\n  State<GroupPanel> createState() => _GroupPanelState();\n}\n\nclass _GroupPanelState extends State<GroupPanel> {\n  final Box<dynamic> localCache = GStrorage.localCache;\n  late Future _futureBuilderFuture;\n  late List<MemberTagItemModel> tagsList;\n  bool showDefault = true;\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = MemberHttp.followUpTags();\n  }\n\n  void onSave() async {\n    feedBack();\n    // 是否有选中的 有选中的带id，没选使用默认0\n    final bool anyHasChecked =\n        tagsList.any((MemberTagItemModel e) => e.checked == true);\n    late String tagids;\n    if (anyHasChecked) {\n      final List<MemberTagItemModel> checkedList =\n          tagsList.where((MemberTagItemModel e) => e.checked == true).toList();\n      final List<int> tagidList =\n          checkedList.map<int>((e) => e.tagid!).toList();\n      tagids = tagidList.join(',');\n    } else {\n      tagids = '0';\n    }\n    // 保存\n    final res = await MemberHttp.addUsers(widget.mid, tagids);\n    SmartDialog.showToast(res['msg']);\n    if (res['status']) {\n      Get.back();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: <Widget>[\n        AppBar(\n          centerTitle: false,\n          elevation: 0,\n          leading: IconButton(\n              onPressed: () => Get.back(),\n              icon: const Icon(Icons.close_outlined)),\n          title: Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium),\n        ),\n        Expanded(\n          child: Material(\n            child: FutureBuilder(\n              future: _futureBuilderFuture,\n              builder: (BuildContext context, AsyncSnapshot snapshot) {\n                if (snapshot.connectionState == ConnectionState.done) {\n                  Map data = snapshot.data as Map;\n                  if (data['status']) {\n                    tagsList = data['data'];\n                    return ListView.builder(\n                      controller: widget.scrollController,\n                      itemCount: data['data'].length,\n                      itemBuilder: (context, index) {\n                        return ListTile(\n                          onTap: () {\n                            data['data'][index].checked =\n                                !data['data'][index].checked;\n                            showDefault =\n                                !data['data'].any((e) => e.checked == true);\n                            setState(() {});\n                          },\n                          dense: true,\n                          leading: const Icon(Icons.group_outlined),\n                          minLeadingWidth: 0,\n                          title: Text(data['data'][index].name),\n                          subtitle: data['data'][index].tip != ''\n                              ? Text(data['data'][index].tip)\n                              : null,\n                          trailing: Transform.scale(\n                            scale: 0.9,\n                            child: Checkbox(\n                              value: data['data'][index].checked,\n                              onChanged: (bool? checkValue) {\n                                data['data'][index].checked = checkValue;\n                                showDefault =\n                                    !data['data'].any((e) => e.checked == true);\n                                setState(() {});\n                              },\n                            ),\n                          ),\n                        );\n                      },\n                    );\n                  } else {\n                    return HttpError(\n                      errMsg: data['msg'],\n                      fn: () => setState(() {}),\n                    );\n                  }\n                } else {\n                  // 骨架屏\n                  return const Text('请求中');\n                }\n              },\n            ),\n          ),\n        ),\n        Divider(\n          height: 1,\n          color: Theme.of(context).disabledColor.withOpacity(0.08),\n        ),\n        Padding(\n          padding: EdgeInsets.only(\n            left: 20,\n            right: 20,\n            top: 12,\n            bottom: MediaQuery.of(context).padding.bottom + 12,\n          ),\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.end,\n            children: [\n              TextButton(\n                onPressed: () => onSave(),\n                style: TextButton.styleFrom(\n                  padding: const EdgeInsets.only(left: 30, right: 30),\n                  foregroundColor: Theme.of(context).colorScheme.onPrimary,\n                  backgroundColor:\n                      Theme.of(context).colorScheme.primary, // 设置按钮背景色\n                ),\n                child: Text(showDefault ? '保存至默认分组' : '保存'),\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/intro_detail.dart",
    "content": "import 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass IntroDetail extends StatelessWidget {\n  const IntroDetail({\n    super.key,\n    this.videoDetail,\n  });\n  final dynamic videoDetail;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: double.infinity,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          const SizedBox(height: 4),\n          Row(\n            children: [\n              GestureDetector(\n                onTap: () {\n                  feedBack();\n                  Clipboard.setData(ClipboardData(text: videoDetail!.bvid!));\n                  SmartDialog.showToast('已复制');\n                },\n                child: Text(\n                  videoDetail!.bvid!,\n                  style: TextStyle(\n                      fontSize: 13,\n                      color: Theme.of(context).colorScheme.primary),\n                ),\n              ),\n              const SizedBox(width: 10),\n              GestureDetector(\n                onTap: () {\n                  feedBack();\n                  Clipboard.setData(\n                      ClipboardData(text: videoDetail!.aid!.toString()));\n                  SmartDialog.showToast('已复制');\n                },\n                child: Text(\n                  videoDetail!.aid!.toString(),\n                  style: TextStyle(\n                      fontSize: 13,\n                      color: Theme.of(context).colorScheme.primary),\n                ),\n              )\n            ],\n          ),\n          const SizedBox(height: 4),\n          SelectableRegion(\n            focusNode: FocusNode(),\n            selectionControls: MaterialTextSelectionControls(),\n            child: Text.rich(\n              style: const TextStyle(height: 1.4),\n              TextSpan(\n                children: [\n                  buildContent(context, videoDetail!),\n                ],\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  InlineSpan buildContent(BuildContext context, content) {\n    final List descV2 = content.descV2;\n    // type\n    // 1 普通文本\n    // 2 @用户\n    final List<TextSpan> spanChilds = List.generate(descV2.length, (index) {\n      final currentDesc = descV2[index];\n      switch (currentDesc.type) {\n        case 1:\n          final List<InlineSpan> spanChildren = <InlineSpan>[];\n          final RegExp urlRegExp = RegExp(r'https?://\\S+\\b');\n          final Iterable<Match> matches =\n              urlRegExp.allMatches(currentDesc.rawText);\n\n          int previousEndIndex = 0;\n          for (final Match match in matches) {\n            if (match.start > previousEndIndex) {\n              spanChildren.add(TextSpan(\n                  text: currentDesc.rawText\n                      .substring(previousEndIndex, match.start)));\n            }\n            spanChildren.add(\n              TextSpan(\n                text: match.group(0),\n                style: TextStyle(\n                    color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色\n                recognizer: TapGestureRecognizer()\n                  ..onTap = () {\n                    // 处理点击事件\n                    try {\n                      Get.toNamed(\n                        '/webview',\n                        parameters: {\n                          'url': match.group(0)!,\n                          'type': 'url',\n                          'pageTitle': match.group(0)!,\n                        },\n                      );\n                    } catch (err) {\n                      SmartDialog.showToast(err.toString());\n                    }\n                  },\n              ),\n            );\n            previousEndIndex = match.end;\n          }\n\n          if (previousEndIndex < currentDesc.rawText.length) {\n            spanChildren.add(TextSpan(\n                text: currentDesc.rawText.substring(previousEndIndex)));\n          }\n\n          final TextSpan result = TextSpan(children: spanChildren);\n          return result;\n        case 2:\n          final Color colorSchemePrimary =\n              Theme.of(context).colorScheme.primary;\n          final String heroTag = Utils.makeHeroTag(currentDesc.bizId);\n          return TextSpan(\n            text: '@${currentDesc.rawText}',\n            style: TextStyle(color: colorSchemePrimary),\n            recognizer: TapGestureRecognizer()\n              ..onTap = () {\n                Get.toNamed(\n                  '/member?mid=${currentDesc.bizId}',\n                  arguments: {'face': '', 'heroTag': heroTag},\n                );\n              },\n          );\n        default:\n          return const TextSpan();\n      }\n    });\n    return TextSpan(children: spanChilds);\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/menu_row.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/utils/feed_back.dart';\n\nclass MenuRow extends StatelessWidget {\n  const MenuRow({\n    super.key,\n    this.loadingStatus,\n  });\n  final bool? loadingStatus;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: double.infinity,\n      color: Theme.of(context).colorScheme.surface,\n      padding: const EdgeInsets.only(top: 9, bottom: 9, left: 12),\n      child: SingleChildScrollView(\n        scrollDirection: Axis.horizontal,\n        child: Row(children: [\n          ActionRowLineItem(\n            onTap: () => {},\n            loadingStatus: loadingStatus,\n            text: '推荐',\n            selectStatus: false,\n          ),\n          const SizedBox(width: 8),\n          ActionRowLineItem(\n            onTap: () => {},\n            loadingStatus: loadingStatus,\n            text: '弹幕',\n            selectStatus: false,\n          ),\n          const SizedBox(width: 8),\n          ActionRowLineItem(\n            onTap: () => {},\n            loadingStatus: loadingStatus,\n            text: '评论列表',\n            selectStatus: false,\n          ),\n          const SizedBox(width: 8),\n          ActionRowLineItem(\n            onTap: () => {},\n            loadingStatus: loadingStatus,\n            text: '播放列表',\n            selectStatus: false,\n          ),\n        ]),\n      ),\n    );\n  }\n\n  Widget actionRowLineItem(\n      BuildContext context, Function? onTap, bool? loadingStatus, String? text,\n      {bool selectStatus = false}) {\n    return Material(\n      color: selectStatus\n          ? Theme.of(context).highlightColor.withOpacity(0.2)\n          : Colors.transparent,\n      borderRadius: const BorderRadius.all(Radius.circular(30)),\n      clipBehavior: Clip.hardEdge,\n      child: InkWell(\n        onTap: () => {\n          feedBack(),\n          onTap!(),\n        },\n        child: Container(\n          padding: const EdgeInsets.fromLTRB(13, 5.5, 13, 4.5),\n          decoration: BoxDecoration(\n            borderRadius: const BorderRadius.all(Radius.circular(30)),\n            border: Border.all(\n              color: selectStatus\n                  ? Colors.transparent\n                  : Theme.of(context).highlightColor.withOpacity(0.2),\n            ),\n          ),\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              AnimatedOpacity(\n                opacity: loadingStatus! ? 0 : 1,\n                duration: const Duration(milliseconds: 200),\n                child: Text(\n                  text!,\n                  style: TextStyle(\n                      fontSize: 13,\n                      color: selectStatus\n                          ? Theme.of(context).colorScheme.onSurface\n                          : Theme.of(context).colorScheme.outline),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass ActionRowLineItem extends StatelessWidget {\n  const ActionRowLineItem({\n    super.key,\n    this.selectStatus,\n    this.onTap,\n    this.text,\n    this.loadingStatus = false,\n  });\n  final bool? selectStatus;\n  final Function? onTap;\n  final bool? loadingStatus;\n  final String? text;\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: selectStatus!\n          ? Theme.of(context).colorScheme.secondaryContainer\n          : Colors.transparent,\n      borderRadius: const BorderRadius.all(Radius.circular(30)),\n      clipBehavior: Clip.hardEdge,\n      child: InkWell(\n        onTap: () => {\n          feedBack(),\n          onTap!(),\n        },\n        child: Container(\n          padding: const EdgeInsets.fromLTRB(13, 5.5, 13, 4.5),\n          decoration: BoxDecoration(\n            borderRadius: const BorderRadius.all(Radius.circular(30)),\n            border: Border.all(\n              color: selectStatus!\n                  ? Colors.transparent\n                  : Theme.of(context).colorScheme.secondaryContainer,\n            ),\n          ),\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              AnimatedOpacity(\n                opacity: loadingStatus! ? 0 : 1,\n                duration: const Duration(milliseconds: 200),\n                child: Text(\n                  text!,\n                  style: TextStyle(\n                      fontSize: 13,\n                      color: selectStatus!\n                          ? Theme.of(context).colorScheme.onSecondaryContainer\n                          : Theme.of(context).colorScheme.outline),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/page_panel.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/models/video_detail_res.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/pages/video/detail/introduction/index.dart';\nimport '../../../../../common/pages_bottom_sheet.dart';\nimport '../../../../../models/common/video_episode_type.dart';\n\nclass PagesPanel extends StatefulWidget {\n  const PagesPanel({\n    super.key,\n    required this.pages,\n    required this.cid,\n    this.sheetHeight,\n    this.changeFuc,\n    required this.videoIntroCtr,\n  });\n  final List<Part> pages;\n  final int cid;\n  final double? sheetHeight;\n  final Function? changeFuc;\n  final VideoIntroController videoIntroCtr;\n\n  @override\n  State<PagesPanel> createState() => _PagesPanelState();\n}\n\nclass _PagesPanelState extends State<PagesPanel> {\n  late List<Part> episodes;\n  late int cid;\n  late RxInt currentIndex = (-1).obs;\n  final String heroTag = Get.arguments['heroTag'];\n  late VideoDetailController _videoDetailController;\n  final ScrollController listViewScrollCtr = ScrollController();\n  late PersistentBottomSheetController? _bottomSheetController;\n\n  @override\n  void initState() {\n    super.initState();\n    cid = widget.cid;\n    episodes = widget.pages;\n    _videoDetailController = Get.find<VideoDetailController>(tag: heroTag);\n    currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid);\n    scrollToIndex();\n    _videoDetailController.cid.listen((int p0) {\n      cid = p0;\n      currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid);\n      scrollToIndex();\n    });\n  }\n\n  @override\n  void dispose() {\n    listViewScrollCtr.dispose();\n    super.dispose();\n  }\n\n  void changeFucCall(item, i) async {\n    widget.changeFuc?.call(item.cid, item.cover);\n    currentIndex.value = i;\n    _bottomSheetController?.close();\n    scrollToIndex();\n  }\n\n  void scrollToIndex() {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      // 在回调函数中获取更新后的状态\n      final double offset = min((currentIndex * 150) - 75,\n          listViewScrollCtr.position.maxScrollExtent);\n      if (currentIndex.value == 0) {\n        listViewScrollCtr.jumpTo(0);\n      } else {\n        listViewScrollCtr.animateTo(\n          offset,\n          duration: const Duration(milliseconds: 300),\n          curve: Curves.easeInOut,\n        );\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: <Widget>[\n        Padding(\n          padding: const EdgeInsets.only(top: 10, bottom: 2),\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            children: [\n              const Text('视频选集 '),\n              Expanded(\n                child: Obx(() => Text(\n                      ' 正在播放：${widget.pages[currentIndex.value].pagePart}',\n                      overflow: TextOverflow.ellipsis,\n                      style: TextStyle(\n                        fontSize: 12,\n                        color: Theme.of(context).colorScheme.outline,\n                      ),\n                    )),\n              ),\n              const SizedBox(width: 10),\n              SizedBox(\n                height: 34,\n                child: TextButton(\n                  style: ButtonStyle(\n                    padding: MaterialStateProperty.all(EdgeInsets.zero),\n                  ),\n                  onPressed: () {\n                    widget.videoIntroCtr.bottomSheetController =\n                        _bottomSheetController = EpisodeBottomSheet(\n                      currentCid: cid,\n                      episodes: episodes,\n                      changeFucCall: changeFucCall,\n                      sheetHeight: widget.sheetHeight,\n                      dataType: VideoEpidoesType.videoPart,\n                      context: context,\n                    ).show(context);\n                  },\n                  child: Text(\n                    '共${widget.pages.length}集',\n                    style: const TextStyle(fontSize: 13),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n        Container(\n          height: 55,\n          margin: const EdgeInsets.only(bottom: 8),\n          child: ListView.builder(\n            scrollDirection: Axis.horizontal,\n            controller: listViewScrollCtr,\n            itemCount: widget.pages.length,\n            itemExtent: 150,\n            itemBuilder: (BuildContext context, int i) {\n              bool isCurrentIndex = currentIndex.value == i;\n              return Container(\n                width: 150,\n                margin: const EdgeInsets.only(right: 10),\n                child: Material(\n                  color: Theme.of(context).colorScheme.onInverseSurface,\n                  borderRadius: BorderRadius.circular(6),\n                  clipBehavior: Clip.hardEdge,\n                  child: InkWell(\n                    onTap: () => changeFucCall(widget.pages[i], i),\n                    child: Padding(\n                      padding: const EdgeInsets.symmetric(\n                          vertical: 8, horizontal: 8),\n                      child: Row(\n                        children: <Widget>[\n                          if (isCurrentIndex) ...<Widget>[\n                            Image.asset(\n                              'assets/images/live.gif',\n                              color: Theme.of(context).colorScheme.primary,\n                              height: 12,\n                            ),\n                            const SizedBox(width: 6)\n                          ],\n                          Expanded(\n                              child: Text(\n                            widget.pages[i].pagePart!,\n                            maxLines: 2,\n                            style: TextStyle(\n                                fontSize: 13,\n                                color: isCurrentIndex\n                                    ? Theme.of(context).colorScheme.primary\n                                    : Theme.of(context).colorScheme.onSurface),\n                            overflow: TextOverflow.ellipsis,\n                          ))\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        )\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/season_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/pages_bottom_sheet.dart';\nimport 'package:pilipala/models/video_detail_res.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport '../../../../../models/common/video_episode_type.dart';\nimport '../controller.dart';\n\nclass SeasonPanel extends StatefulWidget {\n  const SeasonPanel({\n    super.key,\n    required this.ugcSeason,\n    this.cid,\n    this.sheetHeight,\n    this.changeFuc,\n    required this.videoIntroCtr,\n  });\n  final UgcSeason ugcSeason;\n  final int? cid;\n  final double? sheetHeight;\n  final Function? changeFuc;\n  final VideoIntroController videoIntroCtr;\n\n  @override\n  State<SeasonPanel> createState() => _SeasonPanelState();\n}\n\nclass _SeasonPanelState extends State<SeasonPanel> {\n  late List<EpisodeItem> episodes;\n  late int cid;\n  late RxInt currentIndex = (-1).obs;\n  final String heroTag = Get.arguments['heroTag'];\n  late VideoDetailController _videoDetailController;\n  late PersistentBottomSheetController? _bottomSheetController;\n\n  @override\n  void initState() {\n    super.initState();\n    cid = widget.cid!;\n    _videoDetailController = Get.find<VideoDetailController>(tag: heroTag);\n\n    /// 根据 cid 找到对应集，找到对应 episodes\n    /// 有多个episodes时，只显示其中一个\n    /// TODO 同时显示多个合集\n    final List<SectionItem> sections = widget.ugcSeason.sections!;\n    for (int i = 0; i < sections.length; i++) {\n      final List<EpisodeItem> episodesList = sections[i].episodes!;\n      for (int j = 0; j < episodesList.length; j++) {\n        if (episodesList[j].cid == cid) {\n          episodes = episodesList;\n          continue;\n        }\n      }\n    }\n\n    /// 取对应 season_id 的 episodes\n    currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);\n    _videoDetailController.cid.listen((int p0) {\n      cid = p0;\n      currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);\n    });\n  }\n\n  void changeFucCall(item, int i) async {\n    widget.changeFuc?.call(\n      IdUtils.av2bv(item.aid),\n      item.cid,\n      item.aid,\n      item.cover,\n    );\n    currentIndex.value = i;\n    _bottomSheetController?.close();\n  }\n\n  Widget buildEpisodeListItem(\n    EpisodeItem episode,\n    int index,\n    bool isCurrentIndex,\n  ) {\n    Color primary = Theme.of(context).colorScheme.primary;\n    return ListTile(\n      onTap: () => changeFucCall(episode, index),\n      dense: false,\n      leading: isCurrentIndex\n          ? Image.asset(\n              'assets/images/live.gif',\n              color: primary,\n              height: 12,\n            )\n          : null,\n      title: Text(\n        episode.title!,\n        style: TextStyle(\n          fontSize: 14,\n          color: isCurrentIndex\n              ? primary\n              : Theme.of(context).colorScheme.onSurface,\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Builder(builder: (BuildContext context) {\n      return Container(\n        margin: const EdgeInsets.only(\n          top: 8,\n          left: 2,\n          right: 2,\n          bottom: 2,\n        ),\n        child: Material(\n          color: Theme.of(context).colorScheme.onInverseSurface,\n          borderRadius: BorderRadius.circular(6),\n          clipBehavior: Clip.hardEdge,\n          child: InkWell(\n            onTap: () {\n              widget.videoIntroCtr.bottomSheetController =\n                  _bottomSheetController = EpisodeBottomSheet(\n                currentCid: cid,\n                episodes: episodes,\n                changeFucCall: changeFucCall,\n                sheetHeight: widget.sheetHeight,\n                dataType: VideoEpidoesType.videoEpisode,\n                context: context,\n              ).show(context);\n            },\n            child: Padding(\n              padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),\n              child: Row(\n                children: <Widget>[\n                  Expanded(\n                    child: Text(\n                      '合集：${widget.ugcSeason.title!}',\n                      style: Theme.of(context).textTheme.labelMedium,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ),\n                  const SizedBox(width: 15),\n                  Image.asset(\n                    'assets/images/live.gif',\n                    color: Theme.of(context).colorScheme.primary,\n                    height: 12,\n                  ),\n                  const SizedBox(width: 10),\n                  Obx(() => Text(\n                        '${currentIndex.value + 1}/${episodes.length}',\n                        style: Theme.of(context).textTheme.labelMedium,\n                      )),\n                  const SizedBox(width: 6),\n                  const Icon(\n                    Icons.arrow_forward_ios_outlined,\n                    size: 13,\n                  )\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/introduction/widgets/staff_up_item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/video_detail_res.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass StaffUpItem extends StatelessWidget {\n  final Staff item;\n\n  const StaffUpItem({\n    super.key,\n    required this.item,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final String heroTag = Utils.makeHeroTag(item.mid);\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        const SizedBox(height: 15),\n        GestureDetector(\n          onTap: () => Get.toNamed(\n            '/member?mid=${item.mid}',\n            arguments: {'face': item.face, 'heroTag': heroTag},\n          ),\n          child: Hero(\n            tag: heroTag,\n            child: NetworkImgLayer(\n              width: 45,\n              height: 45,\n              src: item.face,\n              type: 'avatar',\n            ),\n          ),\n        ),\n        Padding(\n          padding: const EdgeInsets.only(top: 4),\n          child: SizedBox(\n            width: 85,\n            child: Text(\n              item.name!,\n              overflow: TextOverflow.ellipsis,\n              softWrap: false,\n              textAlign: TextAlign.center,\n              style: TextStyle(\n                color: item.vip!.status == 1\n                    ? const Color.fromARGB(255, 251, 100, 163)\n                    : null,\n              ),\n            ),\n          ),\n        ),\n        Padding(\n          padding: const EdgeInsets.only(top: 4),\n          child: SizedBox(\n            width: 85,\n            child: Text(\n              item.title!,\n              overflow: TextOverflow.ellipsis,\n              softWrap: false,\n              textAlign: TextAlign.center,\n              style: TextStyle(\n                color: Theme.of(context).colorScheme.outline,\n                fontSize: 12,\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/related/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/video.dart';\nimport '../../../../models/model_hot_video_item.dart';\n\nclass ReleatedController extends GetxController {\n  // 视频aid\n  String bvid = Get.parameters['bvid'] ?? \"\";\n  // 推荐视频列表\n  RxList relatedVideoList = <HotVideoItemModel>[].obs;\n\n  OverlayEntry? popupDialog;\n\n  Future<dynamic> queryRelatedVideo() async {\n    return VideoHttp.relatedVideoList(bvid: bvid).then((value) {\n      if (value['status']) {\n        relatedVideoList.value = value['data'];\n      }\n      return value;\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/related/index.dart",
    "content": "library releated_video_panel;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/video/detail/related/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_card_h.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/common/widgets/video_card_h.dart';\nimport './controller.dart';\n\nclass RelatedVideoPanel extends StatefulWidget {\n  const RelatedVideoPanel({super.key});\n\n  @override\n  State<RelatedVideoPanel> createState() => _RelatedVideoPanelState();\n}\n\nclass _RelatedVideoPanelState extends State<RelatedVideoPanel>\n    with AutomaticKeepAliveClientMixin {\n  late ReleatedController _releatedController;\n  late Future _futureBuilder;\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    _releatedController =\n        Get.put(ReleatedController(), tag: Get.arguments?['heroTag']);\n    _futureBuilder = _releatedController.queryRelatedVideo();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return FutureBuilder(\n      future: _futureBuilder,\n      builder: (BuildContext context, AsyncSnapshot snapshot) {\n        if (snapshot.connectionState == ConnectionState.done) {\n          if (snapshot.data == null) {\n            return const SliverToBoxAdapter(child: SizedBox());\n          }\n          if (snapshot.data!['status'] && snapshot.data != null) {\n            RxList relatedVideoList = _releatedController.relatedVideoList;\n            // 请求成功\n            return Obx(\n              () => SliverList(\n                delegate: SliverChildBuilderDelegate((context, index) {\n                  if (index == relatedVideoList.length) {\n                    return SizedBox(\n                        height: MediaQuery.of(context).padding.bottom);\n                  } else {\n                    return Material(\n                      child: VideoCardH(\n                        videoItem: relatedVideoList[index],\n                        showPubdate: true,\n                      ),\n                    );\n                  }\n                }, childCount: relatedVideoList.length + 1),\n              ),\n            );\n          } else {\n            // 请求错误\n            return HttpError(errMsg: '出错了', fn: () {});\n          }\n        } else {\n          // 骨架屏\n          return SliverList(\n            delegate: SliverChildBuilderDelegate((context, index) {\n              return const VideoCardHSkeleton();\n            }, childCount: 5),\n          );\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply/controller.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/reply.dart';\nimport 'package:pilipala/models/common/reply_sort_type.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nclass VideoReplyController extends GetxController {\n  VideoReplyController(\n    this.aid,\n    this.rpid,\n    this.replyLevel,\n  );\n  // 视频aid 请求时使用的oid\n  int? aid;\n  // 层级 2为楼中楼\n  String? replyLevel;\n  // rpid 请求楼中楼回复\n  String? rpid;\n  RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;\n  // 当前页\n  int currentPage = 0;\n  bool isLoadingMore = false;\n  RxString noMore = ''.obs;\n  int ps = 20;\n  RxInt count = 0.obs;\n  // 当前回复的回复\n  ReplyItemModel? currentReplyItem;\n\n  ReplySortType _sortType = ReplySortType.time;\n  RxString sortTypeTitle = ReplySortType.time.titles.obs;\n  RxString sortTypeLabel = ReplySortType.time.labels.obs;\n\n  Box setting = GStrorage.setting;\n  RxInt replyReqCode = 200.obs;\n\n  @override\n  void onInit() {\n    super.onInit();\n    int deaultReplySortIndex =\n        setting.get(SettingBoxKey.replySortType, defaultValue: 0) as int;\n    if (deaultReplySortIndex == 2) {\n      setting.put(SettingBoxKey.replySortType, 0);\n      deaultReplySortIndex = 0;\n    }\n    _sortType = ReplySortType.values[deaultReplySortIndex];\n    sortTypeTitle.value = _sortType.titles;\n    sortTypeLabel.value = _sortType.labels;\n  }\n\n  Future queryReplyList({type = 'init'}) async {\n    if (isLoadingMore) {\n      return;\n    }\n    isLoadingMore = true;\n    if (type == 'init') {\n      currentPage = 0;\n      noMore.value = '';\n    }\n    if (noMore.value == '没有更多了') {\n      isLoadingMore = false;\n      return;\n    }\n    final res = await ReplyHttp.replyList(\n      oid: aid!,\n      pageNum: currentPage + 1,\n      ps: ps,\n      type: ReplyType.video.index,\n      sort: _sortType.index,\n    );\n    if (res['status']) {\n      final List<ReplyItemModel> replies = res['data'].replies;\n      if (replies.isNotEmpty) {\n        noMore.value = '加载中...';\n\n        /// 第一页回复数小于20\n        if (currentPage == 0 && replies.length < 18) {\n          noMore.value = '没有更多了';\n        }\n        currentPage++;\n\n        if (replyList.length == res['data'].page.acount) {\n          noMore.value = '没有更多了';\n        }\n      } else {\n        // 未登录状态replies可能返回null\n        noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';\n      }\n      if (type == 'init') {\n        // 添加置顶回复\n        if (res['data'].upper.top != null) {\n          final bool flag = res['data'].topReplies.any((ReplyItemModel reply) =>\n              reply.rpid == res['data'].upper.top.rpid) as bool;\n          if (!flag) {\n            replies.insert(0, res['data'].upper.top);\n          }\n        }\n        replies.insertAll(0, res['data'].topReplies);\n        count.value = res['data'].page.count;\n        replyList.value = replies;\n      } else {\n        replyList.addAll(replies);\n      }\n    }\n    replyReqCode.value = res['code'];\n    isLoadingMore = false;\n    return res;\n  }\n\n  // 上拉加载\n  Future onLoad() async {\n    queryReplyList(type: 'onLoad');\n  }\n\n  // 排序搜索评论\n  queryBySort() {\n    EasyThrottle.throttle('queryBySort', const Duration(seconds: 1), () {\n      feedBack();\n      switch (_sortType) {\n        case ReplySortType.time:\n          _sortType = ReplySortType.like;\n          break;\n        case ReplySortType.like:\n          _sortType = ReplySortType.time;\n          break;\n        default:\n      }\n      sortTypeTitle.value = _sortType.titles;\n      sortTypeLabel.value = _sortType.labels;\n      currentPage = 0;\n      noMore.value = '';\n      replyList.clear();\n      queryReplyList(type: 'init');\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply/index.dart",
    "content": "library video_reply_panel;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/video/detail/reply/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/skeleton/video_reply.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/pages/video/detail/reply_new/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'controller.dart';\nimport 'widgets/reply_item.dart';\n\nclass VideoReplyPanel extends StatefulWidget {\n  final String? bvid;\n  final int? oid;\n  final int rpid;\n  final String? replyLevel;\n  final Function(ScrollController)? onControllerCreated;\n\n  const VideoReplyPanel({\n    this.bvid,\n    this.oid,\n    this.rpid = 0,\n    this.replyLevel,\n    this.onControllerCreated,\n    super.key,\n  });\n\n  @override\n  State<VideoReplyPanel> createState() => _VideoReplyPanelState();\n}\n\nclass _VideoReplyPanelState extends State<VideoReplyPanel>\n    with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {\n  late VideoReplyController _videoReplyController;\n  late AnimationController fabAnimationCtr;\n  late ScrollController scrollController;\n\n  Future? _futureBuilderFuture;\n  bool _isFabVisible = true;\n  String replyLevel = '1';\n  late String heroTag;\n\n  // 添加页面缓存\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    // int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;\n    heroTag = Get.arguments['heroTag'];\n    replyLevel = widget.replyLevel ?? '1';\n    if (replyLevel == '2') {\n      _videoReplyController = Get.put(\n          VideoReplyController(widget.oid, widget.rpid.toString(), replyLevel),\n          tag: widget.rpid.toString());\n    } else {\n      _videoReplyController = Get.put(\n          VideoReplyController(widget.oid, '', replyLevel),\n          tag: heroTag);\n    }\n\n    fabAnimationCtr = AnimationController(\n        vsync: this, duration: const Duration(milliseconds: 300));\n\n    _futureBuilderFuture = _videoReplyController.queryReplyList();\n    scrollController = ScrollController();\n    widget.onControllerCreated?.call(scrollController);\n    fabAnimationCtr.forward();\n    scrollListener();\n  }\n\n  void scrollListener() {\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),\n              () {\n            _videoReplyController.onLoad();\n          });\n        }\n\n        final ScrollDirection direction =\n            scrollController.position.userScrollDirection;\n        if (direction == ScrollDirection.forward) {\n          _showFab();\n        } else if (direction == ScrollDirection.reverse) {\n          _hideFab();\n        }\n      },\n    );\n  }\n\n  void _showFab() {\n    if (!_isFabVisible) {\n      _isFabVisible = true;\n      fabAnimationCtr.forward();\n    }\n  }\n\n  void _hideFab() {\n    if (_isFabVisible) {\n      _isFabVisible = false;\n      fabAnimationCtr.reverse();\n    }\n  }\n\n  // 展示二级回复\n  void replyReply(replyItem, currentReply, loadMore) {\n    final VideoDetailController videoDetailCtr =\n        Get.find<VideoDetailController>(tag: heroTag);\n    if (replyItem != null) {\n      videoDetailCtr.oid.value = replyItem.oid;\n      videoDetailCtr.fRpid = replyItem.rpid!;\n      videoDetailCtr.firstFloor = replyItem;\n      videoDetailCtr.showReplyReplyPanel(\n          replyItem.oid, replyItem.rpid!, replyItem, currentReply, loadMore);\n    }\n  }\n\n  @override\n  void dispose() {\n    scrollController.removeListener(() {});\n    fabAnimationCtr.dispose();\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return RefreshIndicator(\n      onRefresh: () async {\n        return await _videoReplyController.queryReplyList(type: 'init');\n      },\n      child: Stack(\n        children: [\n          CustomScrollView(\n            controller: scrollController,\n            physics: const AlwaysScrollableScrollPhysics(),\n            key: const PageStorageKey<String>('评论'),\n            slivers: <Widget>[\n              SliverPersistentHeader(\n                pinned: false,\n                floating: true,\n                delegate: _MySliverPersistentHeaderDelegate(\n                  child: Container(\n                    height: 40,\n                    padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),\n                    decoration: BoxDecoration(\n                      color: Theme.of(context).colorScheme.surface,\n                      boxShadow: [\n                        BoxShadow(\n                          color: Theme.of(context).colorScheme.surface,\n                          blurRadius: 0.0,\n                          spreadRadius: 0.0,\n                          offset: const Offset(2, 0),\n                        ),\n                      ],\n                    ),\n                    child: Row(\n                      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                      children: [\n                        Obx(\n                          () => Text(\n                            '${_videoReplyController.sortTypeLabel.value}评论',\n                            style: const TextStyle(fontSize: 13),\n                          ),\n                        ),\n                        SizedBox(\n                          height: 35,\n                          child: TextButton.icon(\n                            onPressed: () =>\n                                _videoReplyController.queryBySort(),\n                            icon: const Icon(Icons.sort, size: 16),\n                            label: Obx(\n                              () => Text(\n                                _videoReplyController.sortTypeLabel.value,\n                                style: const TextStyle(fontSize: 13),\n                              ),\n                            ),\n                          ),\n                        )\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n              FutureBuilder(\n                future: _futureBuilderFuture,\n                builder: (BuildContext context, snapshot) {\n                  if (snapshot.connectionState == ConnectionState.done) {\n                    var data = snapshot.data;\n                    if (_videoReplyController.replyList.isNotEmpty ||\n                        (data != null && data['status'])) {\n                      // 请求成功\n                      return Obx(\n                        () => _videoReplyController.isLoadingMore &&\n                                _videoReplyController.replyList.isEmpty\n                            ? SliverList(\n                                delegate: SliverChildBuilderDelegate(\n                                    (BuildContext context, index) {\n                                  return const VideoReplySkeleton();\n                                }, childCount: 5),\n                              )\n                            : SliverList(\n                                delegate: SliverChildBuilderDelegate(\n                                  (BuildContext context, index) {\n                                    double bottom =\n                                        MediaQuery.of(context).padding.bottom;\n                                    if (index ==\n                                        _videoReplyController\n                                            .replyList.length) {\n                                      return Container(\n                                        padding:\n                                            EdgeInsets.only(bottom: bottom),\n                                        height: bottom + 100,\n                                        child: Center(\n                                          child: Obx(\n                                            () => Text(\n                                              _videoReplyController\n                                                  .noMore.value,\n                                              style: TextStyle(\n                                                fontSize: 12,\n                                                color: Theme.of(context)\n                                                    .colorScheme\n                                                    .outline,\n                                              ),\n                                            ),\n                                          ),\n                                        ),\n                                      );\n                                    } else {\n                                      return ReplyItem(\n                                        replyItem: _videoReplyController\n                                            .replyList[index],\n                                        showReplyRow: true,\n                                        replyLevel: replyLevel,\n                                        replyReply: (replyItem, currentReply,\n                                                loadMore) =>\n                                            replyReply(replyItem, currentReply,\n                                                loadMore),\n                                        replyType: ReplyType.video,\n                                      );\n                                    }\n                                  },\n                                  childCount:\n                                      _videoReplyController.replyList.length +\n                                          1,\n                                ),\n                              ),\n                      );\n                    } else {\n                      // 请求错误\n                      return HttpError(\n                        errMsg: data['msg'],\n                        fn: () {\n                          setState(() {\n                            _futureBuilderFuture =\n                                _videoReplyController.queryReplyList();\n                          });\n                        },\n                      );\n                    }\n                  } else {\n                    // 骨架屏\n                    return SliverList(\n                      delegate: SliverChildBuilderDelegate(\n                          (BuildContext context, index) {\n                        return const VideoReplySkeleton();\n                      }, childCount: 5),\n                    );\n                  }\n                },\n              )\n            ],\n          ),\n          Positioned(\n            bottom: MediaQuery.of(context).padding.bottom + 14,\n            right: 14,\n            child: SlideTransition(\n              position: Tween<Offset>(\n                begin: const Offset(0, 2),\n                end: const Offset(0, 0),\n              ).animate(CurvedAnimation(\n                parent: fabAnimationCtr,\n                curve: Curves.easeInOut,\n              )),\n              child: Obx(\n                () => _videoReplyController.replyReqCode.value == 12061\n                    ? const SizedBox()\n                    : FloatingActionButton(\n                        heroTag: null,\n                        onPressed: () {\n                          feedBack();\n                          showModalBottomSheet(\n                            context: context,\n                            isScrollControlled: true,\n                            builder: (BuildContext context) {\n                              return VideoReplyNewDialog(\n                                oid: _videoReplyController.aid ??\n                                    IdUtils.bv2av(Get.parameters['bvid']!),\n                                root: 0,\n                                parent: 0,\n                                replyType: ReplyType.video,\n                              );\n                            },\n                          ).then(\n                            (value) => {\n                              // 完成评论，数据添加\n                              if (value != null && value['data'] != null)\n                                {\n                                  _videoReplyController.replyList\n                                      .add(value['data'])\n                                }\n                            },\n                          );\n                        },\n                        tooltip: '发表评论',\n                        child: const Icon(Icons.reply),\n                      ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {\n  _MySliverPersistentHeaderDelegate({required this.child});\n  final double _minExtent = 40;\n  final double _maxExtent = 40;\n  final Widget child;\n\n  @override\n  Widget build(\n      BuildContext context, double shrinkOffset, bool overlapsContent) {\n    //创建child子组件\n    //shrinkOffset：child偏移值minExtent~maxExtent\n    //overlapsContent：SliverPersistentHeader覆盖其他子组件返回true，否则返回false\n    return child;\n  }\n\n  //SliverPersistentHeader最大高度\n  @override\n  double get maxExtent => _maxExtent;\n\n  //SliverPersistentHeader最小高度\n  @override\n  double get minExtent => _minExtent;\n\n  @override\n  bool shouldRebuild(covariant _MySliverPersistentHeaderDelegate oldDelegate) {\n    return true;\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply/widgets/reply_item.dart",
    "content": "import 'dart:math';\n\nimport 'package:appscheme/appscheme.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/reply.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/pages/main/index.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/pages/video/detail/reply_new/index.dart';\nimport 'package:pilipala/plugin/pl_gallery/index.dart';\nimport 'package:pilipala/plugin/pl_popup/index.dart';\nimport 'package:pilipala/utils/app_scheme.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/utils/url_utils.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'reply_save.dart';\nimport 'zan.dart';\n\nBox setting = GStrorage.setting;\n\nclass ReplyItem extends StatelessWidget {\n  const ReplyItem({\n    this.replyItem,\n    this.addReply,\n    this.replyLevel,\n    this.showReplyRow = true,\n    this.replyReply,\n    this.replyType,\n    this.replySave = false,\n    super.key,\n  });\n  final ReplyItemModel? replyItem;\n  final Function? addReply;\n  final String? replyLevel;\n  final bool? showReplyRow;\n  final Function? replyReply;\n  final ReplyType? replyType;\n  final bool? replySave;\n\n  @override\n  Widget build(BuildContext context) {\n    final bool isOwner = int.parse(replyItem!.member!.mid!) ==\n        (GlobalDataCache().userInfo?.mid ?? -1);\n    return Material(\n      child: InkWell(\n        // 点击整个评论区 评论详情/回复\n        onTap: () {\n          if (replySave!) {\n            return;\n          }\n          feedBack();\n          if (replyReply != null) {\n            replyReply!(replyItem, null, replyItem!.replies!.isNotEmpty);\n          }\n        },\n        onLongPress: () {\n          if (replySave!) {\n            return;\n          }\n          feedBack();\n          showModalBottomSheet(\n            context: context,\n            useRootNavigator: true,\n            isScrollControlled: true,\n            builder: (context) {\n              return MorePanel(\n                item: replyItem,\n                mainFloor: true,\n                isOwner: isOwner,\n              );\n            },\n          );\n        },\n        child: Container(\n          padding: const EdgeInsets.fromLTRB(12, 14, 8, 5),\n          decoration: BoxDecoration(\n              border: Border(\n                  bottom: BorderSide(\n            width: 1,\n            color:\n                Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.5),\n          ))),\n          child: content(context),\n        ),\n      ),\n    );\n  }\n\n  Widget lfAvtar(BuildContext context, String heroTag) {\n    ColorScheme colorScheme = Theme.of(context).colorScheme;\n    return Stack(\n      children: [\n        Hero(\n          tag: heroTag,\n          child: NetworkImgLayer(\n            src: replyItem!.member!.avatar,\n            width: 34,\n            height: 34,\n            type: 'avatar',\n          ),\n        ),\n        if (replyItem!.member!.officialVerify != null &&\n            replyItem!.member!.officialVerify!['type'] == 0)\n          Positioned(\n            right: 0,\n            bottom: 0,\n            child: Container(\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(7),\n                color: colorScheme.surface,\n              ),\n              child: Icon(\n                Icons.offline_bolt,\n                color: colorScheme.primary,\n                size: 16,\n              ),\n            ),\n          ),\n        if (replyItem!.member!.vip!['vipStatus'] > 0 &&\n            replyItem!.member!.vip!['vipType'] == 2)\n          Positioned(\n            right: 0,\n            bottom: 0,\n            child: Container(\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(7),\n                color: colorScheme.background,\n              ),\n              child: Image.asset(\n                'assets/images/big-vip.png',\n                height: 14,\n              ),\n            ),\n          ),\n      ],\n    );\n  }\n\n  Widget content(BuildContext context) {\n    final String heroTag = Utils.makeHeroTag(replyItem!.mid);\n    ColorScheme colorScheme = Theme.of(context).colorScheme;\n    TextTheme textTheme = Theme.of(context).textTheme;\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: <Widget>[\n        /// fix Stack内GestureDetector  onTap无效\n        GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: () {\n            feedBack();\n            Get.toNamed('/member?mid=${replyItem!.mid}', arguments: {\n              'face': replyItem!.member!.avatar!,\n              'heroTag': heroTag\n            });\n          },\n          child: Row(\n            crossAxisAlignment: CrossAxisAlignment.center,\n            mainAxisSize: MainAxisSize.min,\n            children: <Widget>[\n              lfAvtar(context, heroTag),\n              const SizedBox(width: 12),\n              Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Row(\n                    children: [\n                      Text(\n                        replyItem!.member!.uname!,\n                        style: TextStyle(\n                          color: replyItem!.member!.vip!['vipStatus'] > 0\n                              ? const Color.fromARGB(255, 251, 100, 163)\n                              : colorScheme.outline,\n                          fontSize: 13,\n                        ),\n                      ),\n                      Padding(\n                        padding: const EdgeInsets.only(left: 6, right: 6),\n                        child: Image.asset(\n                          'assets/images/lv/lv${replyItem!.member!.level}.png',\n                          height: 11,\n                        ),\n                      ),\n                      if (replyItem!.isUp!)\n                        const PBadge(\n                          text: 'UP',\n                          size: 'small',\n                          stack: 'normal',\n                          fs: 9,\n                        ),\n                    ],\n                  ),\n                  RichText(\n                    text: TextSpan(\n                      children: [\n                        TextSpan(\n                          text: Utils.dateFormat(replyItem!.ctime),\n                          style: TextStyle(\n                            fontSize: textTheme.labelSmall!.fontSize,\n                            color: colorScheme.outline,\n                          ),\n                        ),\n                        if (replyItem!.replyControl != null &&\n                            replyItem!.replyControl!.location != '')\n                          TextSpan(\n                            text: ' • ${replyItem!.replyControl!.location!}',\n                            style: TextStyle(\n                              fontSize: textTheme.labelSmall!.fontSize,\n                              color: colorScheme.outline,\n                            ),\n                          ),\n                        if (replyItem!.invisible!)\n                          TextSpan(\n                            text: ' • 隐藏的评论',\n                            style: TextStyle(\n                              color: colorScheme.outline,\n                              fontSize: textTheme.labelSmall!.fontSize,\n                            ),\n                          ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n        // title\n        Container(\n          margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),\n          child: Text.rich(\n            style: const TextStyle(height: 1.75),\n            maxLines:\n                replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,\n            overflow: TextOverflow.ellipsis,\n            TextSpan(\n              children: [\n                if (replyItem!.isTop!)\n                  const WidgetSpan(\n                    alignment: PlaceholderAlignment.top,\n                    child: PBadge(\n                      text: 'TOP',\n                      size: 'small',\n                      stack: 'normal',\n                      type: 'line',\n                      fs: 9,\n                    ),\n                  ),\n                buildContent(context, replyItem!, replyReply, null),\n              ],\n            ),\n          ),\n        ),\n        // 操作区域\n        bottonAction(context, replyItem!.replyControl, replySave),\n        // 一楼的评论\n        if ((replyItem!.replyControl!.isShow! ||\n                replyItem!.replies!.isNotEmpty) &&\n            showReplyRow!) ...[\n          Padding(\n            padding: const EdgeInsets.only(top: 5, bottom: 12),\n            child: ReplyItemRow(\n              replies: replyItem!.replies,\n              replyControl: replyItem!.replyControl,\n              // f_rpid: replyItem!.rpid,\n              replyItem: replyItem,\n              replyReply: replyReply,\n            ),\n          ),\n        ],\n      ],\n    );\n  }\n\n  // 感谢、回复、复制\n  Widget bottonAction(BuildContext context, replyControl, replySave) {\n    ColorScheme colorScheme = Theme.of(context).colorScheme;\n    TextTheme textTheme = Theme.of(context).textTheme;\n    return Row(\n      children: <Widget>[\n        const SizedBox(width: 32),\n        SizedBox(\n          height: 32,\n          child: TextButton(\n            onPressed: () {\n              feedBack();\n              showModalBottomSheet(\n                context: context,\n                isScrollControlled: true,\n                builder: (builder) {\n                  return VideoReplyNewDialog(\n                    oid: replyItem!.oid,\n                    root: replyItem!.rpid,\n                    parent: replyItem!.rpid,\n                    replyType: replyType,\n                    replyItem: replyItem,\n                  );\n                },\n              ).then((value) => {\n                    // 完成评论，数据添加\n                    if (value != null && value['data'] != null)\n                      {\n                        addReply?.call(value['data'])\n                        // replyControl.replies.add(value['data']),\n                      }\n                  });\n            },\n            child: Row(children: [\n              if (!replySave!) ...[\n                Icon(Icons.reply,\n                    size: 18, color: colorScheme.outline.withOpacity(0.8)),\n                const SizedBox(width: 3),\n                Text(\n                  '回复',\n                  style: TextStyle(\n                    fontSize: textTheme.labelMedium!.fontSize,\n                    color: colorScheme.outline,\n                  ),\n                )\n              ],\n              if (replySave!)\n                Text(\n                  IdUtils.av2bv(replyItem!.oid!),\n                  style: TextStyle(\n                    fontSize: textTheme.labelMedium!.fontSize,\n                    color: colorScheme.outline,\n                  ),\n                ),\n            ]),\n          ),\n        ),\n        const SizedBox(width: 2),\n        if (replyItem!.upAction!.like!) ...[\n          Text(\n            'up主觉得很赞',\n            style: TextStyle(\n                color: colorScheme.primary,\n                fontSize: textTheme.labelMedium!.fontSize),\n          ),\n          const SizedBox(width: 2),\n        ],\n        if (replyItem!.cardLabel!.isNotEmpty &&\n            replyItem!.cardLabel!.contains('热评'))\n          Text(\n            '热评',\n            style: TextStyle(\n                color: colorScheme.primary,\n                fontSize: textTheme.labelMedium!.fontSize),\n          ),\n        const Spacer(),\n        ZanButton(replyItem: replyItem, replyType: replyType),\n        const SizedBox(width: 5)\n      ],\n    );\n  }\n}\n\n// ignore: must_be_immutable\nclass ReplyItemRow extends StatelessWidget {\n  ReplyItemRow({\n    super.key,\n    this.replies,\n    this.replyControl,\n    // this.f_rpid,\n    this.replyItem,\n    this.replyReply,\n  });\n  final List? replies;\n  ReplyControl? replyControl;\n  // int? f_rpid;\n  ReplyItemModel? replyItem;\n  Function? replyReply;\n\n  @override\n  Widget build(BuildContext context) {\n    final bool isShow = replyControl!.isShow!;\n    final int extraRow = replyControl != null && isShow ? 1 : 0;\n    ColorScheme colorScheme = Theme.of(context).colorScheme;\n    TextTheme textTheme = Theme.of(context).textTheme;\n\n    return Container(\n      margin: const EdgeInsets.only(left: 42, right: 4, top: 0),\n      child: Material(\n        color: colorScheme.onInverseSurface,\n        borderRadius: BorderRadius.circular(6),\n        clipBehavior: Clip.hardEdge,\n        animationDuration: Duration.zero,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (replies!.isNotEmpty)\n              for (int i = 0; i < replies!.length; i++) ...[\n                InkWell(\n                  // 一楼点击评论展开评论详情\n                  onTap: () {\n                    replyReply?.call(\n                      replyItem,\n                      replies![i],\n                      replyItem!.replies!.isNotEmpty,\n                    );\n                  },\n                  onLongPress: () {\n                    feedBack();\n                    showModalBottomSheet(\n                      context: context,\n                      useRootNavigator: true,\n                      isScrollControlled: true,\n                      builder: (context) {\n                        return MorePanel(item: replies![i]);\n                      },\n                    );\n                  },\n                  child: Container(\n                    width: double.infinity,\n                    padding: EdgeInsets.fromLTRB(\n                      8,\n                      i == 0 && (extraRow == 1 || replies!.length > 1) ? 8 : 5,\n                      8,\n                      6,\n                    ),\n                    child: Text.rich(\n                      overflow: TextOverflow.ellipsis,\n                      maxLines: 2,\n                      TextSpan(\n                        children: [\n                          TextSpan(\n                            text: replies![i].member.uname + ' ',\n                            style: TextStyle(\n                              fontSize: Theme.of(context)\n                                  .textTheme\n                                  .titleSmall!\n                                  .fontSize,\n                              color: colorScheme.primary,\n                            ),\n                            recognizer: TapGestureRecognizer()\n                              ..onTap = () {\n                                feedBack();\n                                final String heroTag =\n                                    Utils.makeHeroTag(replies![i].member.mid);\n                                Get.toNamed(\n                                    '/member?mid=${replies![i].member.mid}',\n                                    arguments: {\n                                      'face': replies![i].member.avatar,\n                                      'heroTag': heroTag\n                                    });\n                              },\n                          ),\n                          if (replies![i].isUp)\n                            const WidgetSpan(\n                              alignment: PlaceholderAlignment.top,\n                              child: PBadge(\n                                text: 'UP',\n                                size: 'small',\n                                stack: 'normal',\n                                fs: 9,\n                              ),\n                            ),\n                          buildContent(\n                              context, replies![i], replyReply, replyItem),\n                        ],\n                      ),\n                    ),\n                  ),\n                )\n              ],\n            if (extraRow == 1)\n              InkWell(\n                // 一楼点击【共xx条回复】展开评论详情\n                onTap: () => replyReply?.call(replyItem, null, true),\n                onLongPress: () => {},\n                child: Container(\n                  width: double.infinity,\n                  padding: const EdgeInsets.fromLTRB(8, 5, 8, 8),\n                  child: Text.rich(\n                    TextSpan(\n                      style: TextStyle(\n                        fontSize: textTheme.labelMedium!.fontSize,\n                      ),\n                      children: [\n                        if (replyControl!.upReply!)\n                          const TextSpan(text: 'up主等人 '),\n                        TextSpan(\n                          text: replyControl!.entryText!,\n                          style: TextStyle(\n                            color: colorScheme.primary,\n                          ),\n                        )\n                      ],\n                    ),\n                  ),\n                ),\n              )\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nInlineSpan buildContent(\n    BuildContext context, replyItem, replyReply, fReplyItem) {\n  final String routePath = Get.currentRoute;\n  bool isVideoPage = routePath.startsWith('/video');\n  ColorScheme colorScheme = Theme.of(context).colorScheme;\n\n  // replyItem 当前回复内容\n  // replyReply 查看二楼回复（回复详情）回调\n  // fReplyItem 父级回复内容，用作二楼回复（回复详情）展示\n  final content = replyItem.content;\n  final List<InlineSpan> spanChilds = <InlineSpan>[];\n\n  // 投票\n  if (content.vote.isNotEmpty) {\n    content.message.splitMapJoin(RegExp(r\"\\{vote:.*?\\}\"),\n        onMatch: (Match match) {\n      // String matchStr = match[0]!;\n      spanChilds.add(\n        TextSpan(\n          text: '投票: ${content.vote['title']}',\n          style: TextStyle(\n            color: Theme.of(context).colorScheme.primary,\n          ),\n          recognizer: TapGestureRecognizer()\n            ..onTap = () => Get.toNamed(\n                  '/webview',\n                  parameters: {\n                    'url': content.vote['url'],\n                    'type': 'vote',\n                    'pageTitle': content.vote['title'],\n                  },\n                ),\n        ),\n      );\n      return '';\n    }, onNonMatch: (String str) {\n      return str;\n    });\n  }\n  content.message = content.message.replaceAll(RegExp(r\"\\{vote:.*?\\}\"), ' ');\n  content.message = content.message\n      .replaceAll('&amp;', '&')\n      .replaceAll('&lt;', '<')\n      .replaceAll('&gt;', '>')\n      .replaceAll('&quot;', '\"')\n      .replaceAll('&apos;', \"'\")\n      .replaceAll('&nbsp;', ' ');\n  // 构建正则表达式\n  final List<String> specialTokens = [\n    ...content.emote.keys,\n    ...content.topicsMeta?.keys?.map((e) => '#$e#') ?? [],\n    ...content.atNameToMid.keys.map((e) => '@$e'),\n  ];\n  List<dynamic> jumpUrlKeysList = content.jumpUrl.keys.map((e) {\n    return e.replaceAllMapped(\n        RegExp(r'[?+*]'), (match) => '\\\\${match.group(0)}');\n  }).toList();\n\n  String patternStr = specialTokens.map(RegExp.escape).join('|');\n  if (patternStr.isNotEmpty) {\n    patternStr += \"|\";\n  }\n  patternStr += r'(\\b(?:\\d+[:：])?[0-5]?[0-9][:：][0-5]?[0-9]\\b)';\n  if (jumpUrlKeysList.isNotEmpty) {\n    patternStr += '|${jumpUrlKeysList.join('|')}';\n  }\n  RegExp bv23Regex = RegExp(r'https://b23\\.tv/[a-zA-Z0-9]{7}');\n  final RegExp pattern = RegExp(patternStr);\n  List<String> matchedStrs = [];\n  void addPlainTextSpan(str) {\n    spanChilds.add(\n      TextSpan(\n        text: str,\n        // recognizer: TapGestureRecognizer()\n        //   ..onTap = () => replyReply?.call(\n        //         replyItem.root == 0 ? replyItem : fReplyItem,\n        //         replyItem,\n        //         fReplyItem!.replies!.isNotEmpty,\n        //       ),\n      ),\n    );\n  }\n\n  void onPreviewImg(picList, initIndex, randomInt) {\n    final MainController mainController = Get.find<MainController>();\n    mainController.imgPreviewStatus = true;\n    Navigator.of(context).push(\n      HeroDialogRoute<void>(\n        builder: (BuildContext context) => InteractiveviewerGallery(\n          sources: picList,\n          initIndex: initIndex,\n          onPageChanged: (int pageIndex) {},\n          onDismissed: (int value) {\n            print('onDismissed');\n            final MainController mainController = Get.find<MainController>();\n            mainController.imgPreviewStatus = false;\n          },\n        ),\n      ),\n    );\n  }\n\n  // 分割文本并处理每个部分\n  content.message.splitMapJoin(\n    pattern,\n    onMatch: (Match match) {\n      String matchStr = match[0]!;\n      if (content.emote.containsKey(matchStr)) {\n        // 处理表情\n        final int size = content.emote[matchStr]['meta']['size'];\n        spanChilds.add(WidgetSpan(\n          child: NetworkImgLayer(\n            src: content.emote[matchStr]['url'],\n            type: 'emote',\n            width: size * 20,\n            height: size * 20,\n          ),\n        ));\n      } else if (matchStr.startsWith(\"@\") &&\n          content.atNameToMid.containsKey(matchStr.substring(1))) {\n        // 处理@用户\n        final String userName = matchStr.substring(1);\n        final int userId = content.atNameToMid[userName];\n        spanChilds.add(\n          TextSpan(\n            text: matchStr,\n            style: TextStyle(\n              color: colorScheme.primary,\n            ),\n            recognizer: TapGestureRecognizer()\n              ..onTap = () {\n                final String heroTag = Utils.makeHeroTag(userId);\n                Get.toNamed(\n                  '/member?mid=$userId',\n                  arguments: {'face': '', 'heroTag': heroTag},\n                );\n              },\n          ),\n        );\n      } else if (RegExp(r'^\\b(?:\\d+[:：])?[0-5]?[0-9][:：][0-5]?[0-9]\\b$')\n          .hasMatch(matchStr)) {\n        matchStr = matchStr.replaceAll('：', ':');\n        spanChilds.add(\n          TextSpan(\n            text: ' $matchStr ',\n            style: isVideoPage\n                ? TextStyle(\n                    color: colorScheme.primary,\n                  )\n                : null,\n            recognizer: TapGestureRecognizer()\n              ..onTap = () {\n                // 跳转到指定位置\n                if (isVideoPage) {\n                  try {\n                    SmartDialog.showToast('跳转至：$matchStr');\n                    Get.find<VideoDetailController>(\n                            tag: Get.arguments['heroTag'])\n                        .plPlayerController\n                        .seekTo(\n                          Duration(seconds: Utils.duration(matchStr)),\n                        );\n                  } catch (e) {\n                    SmartDialog.showToast('跳转失败: $e');\n                  }\n                }\n              },\n          ),\n        );\n      } else {\n        String appUrlSchema = '';\n        final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,\n            defaultValue: false) as bool;\n        if (content.jumpUrl[matchStr] != null &&\n            !matchedStrs.contains(matchStr)) {\n          appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];\n          if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {\n            addPlainTextSpan(matchStr);\n            return \"\";\n          }\n          spanChilds.addAll(\n            [\n              if (content.jumpUrl[matchStr]?['prefix_icon'] != null) ...[\n                WidgetSpan(\n                  child: Image.network(\n                    content.jumpUrl[matchStr]['prefix_icon'],\n                    height: 19,\n                    color: colorScheme.primary,\n                  ),\n                )\n              ],\n              TextSpan(\n                text: content.jumpUrl[matchStr]['title'],\n                style: TextStyle(\n                  color: colorScheme.primary,\n                ),\n                recognizer: TapGestureRecognizer()\n                  ..onTap = () async {\n                    final String title = content.jumpUrl[matchStr]['title'];\n                    if (appUrlSchema == '') {\n                      if (matchStr.startsWith('BV')) {\n                        UrlUtils.matchUrlPush(\n                          matchStr,\n                          title,\n                          '',\n                        );\n                      } else if (RegExp(r'^cv\\d+$').hasMatch(matchStr)) {\n                        Get.toNamed('/read', parameters: {\n                          'title': title,\n                          'id': Utils.matchNum(matchStr).first.toString(),\n                          'articleType': 'read',\n                        });\n                      } else {\n                        Uri uri = Uri.parse(matchStr.replaceAll('/?', '?'));\n                        SchemeEntity scheme = SchemeEntity(\n                          scheme: uri.scheme,\n                          host: uri.host,\n                          port: uri.port,\n                          path: uri.path,\n                          query: uri.queryParameters,\n                          source: '',\n                          dataString: matchStr,\n                        );\n                        PiliSchame.fullPathPush(scheme);\n                      }\n                    } else {\n                      if (appUrlSchema.startsWith('bilibili://search')) {\n                        Get.toNamed('/searchResult',\n                            parameters: {'keyword': title});\n                      } else if (matchStr.startsWith('https://b23.tv')) {\n                        final String redirectUrl =\n                            await UrlUtils.parseRedirectUrl(matchStr);\n                        final String pathSegment = Uri.parse(redirectUrl).path;\n                        final String lastPathSegment =\n                            pathSegment.split('/').last;\n                        if (lastPathSegment.startsWith('BV')) {\n                          UrlUtils.matchUrlPush(\n                            lastPathSegment,\n                            title,\n                            redirectUrl,\n                          );\n                        } else {\n                          Get.toNamed(\n                            '/webview',\n                            parameters: {\n                              'url': redirectUrl,\n                              'type': 'url',\n                              'pageTitle': title\n                            },\n                          );\n                        }\n                      } else {\n                        Get.toNamed(\n                          '/webview',\n                          parameters: {\n                            'url': matchStr,\n                            'type': 'url',\n                            'pageTitle': title\n                          },\n                        );\n                      }\n                    }\n                  },\n              )\n            ],\n          );\n          // 只显示一次\n          matchedStrs.add(matchStr);\n        } else if (content.topicsMeta.keys.isNotEmpty &&\n            matchStr.length > 1 &&\n            content.topicsMeta[matchStr.substring(1, matchStr.length - 1)] !=\n                null) {\n          spanChilds.add(\n            TextSpan(\n              text: matchStr,\n              style: TextStyle(\n                color: colorScheme.primary,\n              ),\n              recognizer: TapGestureRecognizer()\n                ..onTap = () {\n                  final String topic =\n                      matchStr.substring(1, matchStr.length - 1);\n                  Get.toNamed('/searchResult', parameters: {'keyword': topic});\n                },\n            ),\n          );\n        } else {\n          addPlainTextSpan(matchStr);\n        }\n      }\n      return '';\n    },\n    onNonMatch: (String nonMatchStr) {\n      return nonMatchStr.splitMapJoin(\n        bv23Regex,\n        onMatch: (Match match) {\n          String matchStr = match[0]!;\n          spanChilds.add(\n            TextSpan(\n              text: ' $matchStr ',\n              style: isVideoPage\n                  ? TextStyle(\n                      color: colorScheme.primary,\n                    )\n                  : null,\n              recognizer: TapGestureRecognizer()\n                ..onTap = () => Get.toNamed(\n                      '/webview',\n                      parameters: {\n                        'url': matchStr,\n                        'type': 'url',\n                        'pageTitle': matchStr\n                      },\n                    ),\n            ),\n          );\n          return '';\n        },\n        onNonMatch: (String nonMatchOtherStr) {\n          addPlainTextSpan(nonMatchOtherStr);\n          return nonMatchOtherStr;\n        },\n      );\n    },\n  );\n\n  if (content.jumpUrl.keys.isNotEmpty) {\n    List<String> unmatchedItems = content.jumpUrl.keys\n        .toList()\n        .where((item) => !content.message.contains(item))\n        .toList();\n    if (unmatchedItems.isNotEmpty) {\n      for (int i = 0; i < unmatchedItems.length; i++) {\n        String patternStr = unmatchedItems[i];\n        spanChilds.addAll(\n          [\n            if (content.jumpUrl[patternStr]?['prefix_icon'] != null) ...[\n              WidgetSpan(\n                child: Image.network(\n                  content.jumpUrl[patternStr]['prefix_icon'],\n                  height: 19,\n                  color: colorScheme.primary,\n                ),\n              )\n            ],\n            TextSpan(\n              text: content.jumpUrl[patternStr]['title'],\n              style: TextStyle(\n                color: colorScheme.primary,\n              ),\n              recognizer: TapGestureRecognizer()\n                ..onTap = () {\n                  Get.toNamed(\n                    '/webview',\n                    parameters: {\n                      'url': patternStr,\n                      'type': 'url',\n                      'pageTitle': content.jumpUrl[patternStr]['title']\n                    },\n                  );\n                },\n            )\n          ],\n        );\n      }\n    }\n  }\n  // 图片渲染\n  if (content.pictures.isNotEmpty) {\n    final List<String> picList = <String>[];\n    final int len = content.pictures.length;\n    spanChilds.add(const TextSpan(text: '\\n'));\n    if (len == 1) {\n      Map pictureItem = content.pictures.first;\n      picList.add(pictureItem['img_src']);\n      spanChilds.add(\n        WidgetSpan(\n          child: LayoutBuilder(\n            builder: (BuildContext context, BoxConstraints box) {\n              double maxHeight = box.maxWidth * 0.6; // 设置最大高度\n              // double width = (box.maxWidth / 2).truncateToDouble();\n              double height = 100;\n              try {\n                height = ((box.maxWidth /\n                        2 *\n                        pictureItem['img_height'] /\n                        pictureItem['img_width']))\n                    .truncateToDouble();\n              } catch (_) {}\n              String randomInt = Random().nextInt(101).toString();\n\n              return Hero(\n                tag: picList[0] + randomInt,\n                child: GestureDetector(\n                  onTap: () => onPreviewImg(picList, 0, randomInt),\n                  child: Container(\n                    padding: const EdgeInsets.only(top: 4),\n                    constraints: BoxConstraints(maxHeight: maxHeight),\n                    width: box.maxWidth / 2,\n                    height: height,\n                    child: Stack(\n                      children: [\n                        Positioned.fill(\n                          child: NetworkImgLayer(\n                            src: picList[0],\n                            width: box.maxWidth / 2,\n                            height: height,\n                          ),\n                        ),\n                        height > Get.size.height * 0.9\n                            ? const PBadge(\n                                text: '长图',\n                                right: 8,\n                                bottom: 8,\n                              )\n                            : const SizedBox(),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      );\n    } else if (len > 1) {\n      List<Widget> list = [];\n      for (var i = 0; i < len; i++) {\n        picList.add(content.pictures[i]['img_src']);\n      }\n      for (var i = 0; i < len; i++) {\n        String randomInt = Random().nextInt(101).toString();\n        list.add(\n          LayoutBuilder(\n            builder: (context, BoxConstraints box) {\n              return Hero(\n                tag: picList[i] + randomInt,\n                child: GestureDetector(\n                  onTap: () => onPreviewImg(picList, i, randomInt),\n                  child: NetworkImgLayer(\n                      src: picList[i],\n                      width: box.maxWidth,\n                      height: box.maxWidth,\n                      origAspectRatio: content.pictures[i]['img_width'] /\n                          content.pictures[i]['img_height']),\n                ),\n              );\n            },\n          ),\n        );\n      }\n      spanChilds.add(\n        WidgetSpan(\n          child: LayoutBuilder(\n            builder: (context, BoxConstraints box) {\n              double maxWidth = box.maxWidth;\n              double crossCount = len < 3 ? 2 : 3;\n              double height = maxWidth /\n                      crossCount *\n                      (len % crossCount == 0\n                          ? len ~/ crossCount\n                          : len ~/ crossCount + 1) +\n                  6;\n              return Container(\n                padding: const EdgeInsets.only(top: 6),\n                height: height,\n                child: GridView.count(\n                  padding: EdgeInsets.zero,\n                  physics: const NeverScrollableScrollPhysics(),\n                  crossAxisCount: crossCount.toInt(),\n                  mainAxisSpacing: 4.0,\n                  crossAxisSpacing: 4.0,\n                  childAspectRatio: 1,\n                  children: list,\n                ),\n              );\n            },\n          ),\n        ),\n      );\n    }\n  }\n\n  // 笔记链接\n  if (content.richText.isNotEmpty) {\n    spanChilds.add(\n      TextSpan(\n        text: ' 笔记',\n        style: TextStyle(\n          color: Theme.of(context).colorScheme.primary,\n        ),\n        recognizer: TapGestureRecognizer()\n          ..onTap = () => Get.toNamed(\n                '/webview',\n                parameters: {\n                  'url': content.richText['note']['click_url'],\n                  'type': 'note',\n                  'pageTitle': '笔记预览'\n                },\n              ),\n      ),\n    );\n  }\n  // spanChilds.add(TextSpan(text: matchMember));\n  return TextSpan(children: spanChilds);\n}\n\nclass MorePanel extends StatelessWidget {\n  final dynamic item;\n  final bool mainFloor;\n  final bool isOwner;\n  const MorePanel({\n    super.key,\n    required this.item,\n    this.mainFloor = false,\n    this.isOwner = false,\n  });\n\n  Future<dynamic> menuActionHandler(String type) async {\n    String message = item.content.message ?? item.content;\n    switch (type) {\n      case 'copyAll':\n        await Clipboard.setData(ClipboardData(text: message));\n        SmartDialog.showToast('已复制');\n        Get.back();\n        break;\n      case 'copyFreedom':\n        Get.back();\n        showDialog(\n          context: Get.context!,\n          builder: (context) {\n            return AlertDialog(\n              title: const Text('自由复制'),\n              content: SelectableText(message),\n            );\n          },\n        );\n        break;\n      case 'save':\n        Get.back();\n        Navigator.push(\n          Get.context!,\n          PlPopupRoute(child: ReplySave(replyItem: item)),\n        );\n        break;\n      // case 'block':\n      //   SmartDialog.showToast('加入黑名单');\n      //   break;\n      // case 'report':\n      //   SmartDialog.showToast('举报');\n      //   break;\n      case 'delete':\n        // 删除评论提示\n        await showDialog(\n          context: Get.context!,\n          builder: (context) {\n            return AlertDialog(\n              title: const Text('删除评论'),\n              content: const Text('删除评论后，评论下所有回复将被删除，确定删除吗？'),\n              actions: <Widget>[\n                TextButton(\n                  onPressed: () => Get.back(),\n                  child: Text('取消',\n                      style: TextStyle(\n                          color: Theme.of(context).colorScheme.outline)),\n                ),\n                TextButton(\n                  onPressed: () async {\n                    Get.back();\n                    var result = await ReplyHttp.replyDel(\n                      type: item.type!,\n                      oid: item.oid!,\n                      rpid: item.rpid!,\n                    );\n                    if (result['status']) {\n                      SmartDialog.showToast('评论删除成功，需手动刷新');\n                      Get.back();\n                    } else {\n                      SmartDialog.showToast(result['msg']);\n                    }\n                  },\n                  child: const Text('确定'),\n                ),\n              ],\n            );\n          },\n        );\n        break;\n      default:\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    ColorScheme colorScheme = Theme.of(context).colorScheme;\n    TextTheme textTheme = Theme.of(context).textTheme;\n    Color errorColor = colorScheme.error;\n    return Container(\n      padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          InkWell(\n            onTap: () => Get.back(),\n            child: Container(\n              height: 35,\n              padding: const EdgeInsets.only(bottom: 2),\n              child: Center(\n                child: Container(\n                  width: 32,\n                  height: 3,\n                  decoration: BoxDecoration(\n                      color: colorScheme.outline,\n                      borderRadius: const BorderRadius.all(Radius.circular(3))),\n                ),\n              ),\n            ),\n          ),\n          ListTile(\n            onTap: () async => await menuActionHandler('copyAll'),\n            minLeadingWidth: 0,\n            leading: const Icon(Icons.copy_all_outlined, size: 19),\n            title: Text('复制全部', style: textTheme.titleSmall),\n          ),\n          ListTile(\n            onTap: () async => await menuActionHandler('copyFreedom'),\n            minLeadingWidth: 0,\n            leading: const Icon(Icons.copy_outlined, size: 19),\n            title: Text('自由复制', style: textTheme.titleSmall),\n          ),\n          if (mainFloor && item.content.pictures.isEmpty)\n            ListTile(\n              onTap: () async => await menuActionHandler('save'),\n              minLeadingWidth: 0,\n              leading: const Icon(Icons.save_alt_rounded, size: 19),\n              title: Text('本地保存', style: textTheme.titleSmall),\n            ),\n          // ListTile(\n          //   onTap: () async => await menuActionHandler('block'),\n          //   minLeadingWidth: 0,\n          //   leading: Icon(Icons.block_outlined, color: errorColor),\n          //   title: Text('加入黑名单', style: TextStyle(color: errorColor)),\n          // ),\n          // ListTile(\n          //   onTap: () async => await menuActionHandler('report'),\n          //   minLeadingWidth: 0,\n          //   leading: Icon(Icons.report_outlined, color: errorColor),\n          //   title: Text('举报', style: TextStyle(color: errorColor)),\n          // ),\n          if (isOwner)\n            ListTile(\n              onTap: () async => await menuActionHandler('delete'),\n              minLeadingWidth: 0,\n              leading: Icon(Icons.delete_outline, color: errorColor),\n              title: Text('删除评论',\n                  style: textTheme.titleSmall!.copyWith(color: errorColor)),\n            ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply/widgets/reply_save.dart",
    "content": "import 'dart:math';\nimport 'dart:typed_data';\nimport 'dart:ui';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';\nimport 'package:saver_gallery/saver_gallery.dart';\n\nclass ReplySave extends StatefulWidget {\n  final ReplyItemModel? replyItem;\n  const ReplySave({required this.replyItem, super.key});\n\n  @override\n  State<ReplySave> createState() => _ReplySaveState();\n}\n\nclass _ReplySaveState extends State<ReplySave> {\n  final _boundaryKey = GlobalKey();\n\n  void _generatePicWidget() async {\n    SmartDialog.showLoading(msg: '保存中');\n    try {\n      RenderRepaintBoundary boundary = _boundaryKey.currentContext!\n          .findRenderObject() as RenderRepaintBoundary;\n      var image = await boundary.toImage(pixelRatio: 3);\n      ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);\n      Uint8List pngBytes = byteData!.buffer.asUint8List();\n      String picName =\n          \"plpl_reply_${DateTime.now().toString().replaceAll(RegExp(r'[- :]'), '').split('.').first}\";\n      final result = await SaverGallery.saveImage(\n        Uint8List.fromList(pngBytes),\n        name: '$picName.png',\n        androidRelativePath: \"Pictures/PiliPala\",\n        androidExistNotSave: false,\n      );\n      if (result.isSuccess) {\n        SmartDialog.showToast('保存成功');\n      }\n    } catch (err) {\n      print(err);\n    } finally {\n      SmartDialog.dismiss();\n    }\n  }\n\n  List<Widget> _createWidgets(int count, Widget Function() builder) {\n    return List<Widget>.generate(count, (_) => Expanded(child: builder()));\n  }\n\n  List<Widget> _createColumnWidgets() {\n    return _createWidgets(3, () => Row(children: _createRowWidgets()));\n  }\n\n  List<Widget> _createRowWidgets() {\n    return _createWidgets(\n      4,\n      () => Center(\n        child: Transform.rotate(\n          angle: pi / 10,\n          child: const Text(\n            'PiliPala',\n            style: TextStyle(\n              color: Color(0x08000000),\n              fontSize: 18,\n              fontWeight: FontWeight.bold,\n              decoration: TextDecoration.none,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SafeArea(\n      top: false,\n      bottom: false,\n      child: BackdropFilter(\n        filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),\n        child: Container(\n          width: Get.width,\n          height: Get.height,\n          padding: EdgeInsets.fromLTRB(\n            0,\n            MediaQuery.of(context).padding.top + 4,\n            0,\n            MediaQuery.of(context).padding.bottom + 4,\n          ),\n          color: Colors.black54,\n          child: Column(\n            children: [\n              Expanded(\n                child: Container(\n                  clipBehavior: Clip.antiAlias,\n                  decoration: BoxDecoration(\n                    borderRadius: BorderRadius.circular(20),\n                  ),\n                  child: Center(\n                    child: SingleChildScrollView(\n                      child: RepaintBoundary(\n                        key: _boundaryKey,\n                        child: IntrinsicHeight(\n                          child: Stack(\n                            children: [\n                              ReplyItem(\n                                replyItem: widget.replyItem,\n                                showReplyRow: false,\n                                replySave: true,\n                              ),\n                              Positioned.fill(\n                                child: Column(\n                                  children: _createColumnWidgets(),\n                                ),\n                              ),\n                            ],\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n              const SizedBox(height: 20),\n              Row(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  FilledButton(\n                    onPressed: () => Get.back(),\n                    child: const Text('取消'),\n                  ),\n                  const SizedBox(width: 40),\n                  FilledButton(\n                    onPressed: _generatePicWidget,\n                    child: const Text('保存'),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply/widgets/zan.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:pilipala/http/reply.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/utils/feed_back.dart';\n\nclass ZanButton extends StatefulWidget {\n  const ZanButton({\n    super.key,\n    this.replyItem,\n    this.replyType,\n  });\n\n  final ReplyItemModel? replyItem;\n  final ReplyType? replyType;\n\n  @override\n  State<ZanButton> createState() => _ZanButtonState();\n}\n\nclass _ZanButtonState extends State<ZanButton> {\n  // 评论点赞\n  Future onLikeReply() async {\n    feedBack();\n    // SmartDialog.showLoading(msg: 'pilipala ...');\n    final ReplyItemModel replyItem = widget.replyItem!;\n    final int oid = replyItem.oid!;\n    final int rpid = replyItem.rpid!;\n    // 1 已点赞 2 不喜欢 0 未操作\n    final int action = replyItem.action == 0 ? 1 : 0;\n    final res = await ReplyHttp.likeReply(\n        type: widget.replyType!.index, oid: oid, rpid: rpid, action: action);\n    // SmartDialog.dismiss();\n    if (res['status']) {\n      SmartDialog.showToast(replyItem.action == 0 ? '点赞成功 👍' : '取消赞 💔');\n      if (action == 1) {\n        replyItem.like = replyItem.like! + 1;\n        replyItem.action = 1;\n      } else {\n        replyItem.like = replyItem.like! - 1;\n        replyItem.action = 0;\n      }\n      setState(() {});\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  bool isProcessing = false;\n  void Function()? handleState(Future Function() action) {\n    return isProcessing\n        ? null\n        : () async {\n            setState(() => isProcessing = true);\n            await action();\n            setState(() => isProcessing = false);\n          };\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final ThemeData t = Theme.of(context);\n    final Color color = t.colorScheme.outline;\n    final Color primary = t.colorScheme.primary;\n    return SizedBox(\n      height: 32,\n      child: TextButton(\n        onPressed: handleState(onLikeReply),\n        child: Row(\n          children: [\n            Icon(\n              widget.replyItem!.action == 1\n                  ? FontAwesomeIcons.solidThumbsUp\n                  : FontAwesomeIcons.thumbsUp,\n              size: 16,\n              color: widget.replyItem!.action == 1 ? primary : color,\n            ),\n            const SizedBox(width: 4),\n            AnimatedSwitcher(\n              duration: const Duration(milliseconds: 400),\n              transitionBuilder: (Widget child, Animation<double> animation) {\n                return ScaleTransition(scale: animation, child: child);\n              },\n              child: Text(\n                widget.replyItem!.like.toString(),\n                key: ValueKey<int>(widget.replyItem!.like!),\n                style: TextStyle(\n                  color: widget.replyItem!.action == 1 ? primary : color,\n                  fontSize: t.textTheme.labelSmall!.fontSize,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply_new/index.dart",
    "content": "library video_reply_new;\n\nexport './view.dart';"
  },
  {
    "path": "lib/pages/video/detail/reply_new/toolbar_icon_button.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ToolbarIconButton extends StatelessWidget {\n  final VoidCallback onPressed;\n  final Icon icon;\n  final String toolbarType;\n  final bool selected;\n\n  const ToolbarIconButton({\n    super.key,\n    required this.onPressed,\n    required this.icon,\n    required this.toolbarType,\n    required this.selected,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 36,\n      height: 36,\n      child: IconButton(\n        onPressed: onPressed,\n        icon: icon,\n        highlightColor: Theme.of(context).colorScheme.secondaryContainer,\n        color: selected\n            ? Theme.of(context).colorScheme.onSecondaryContainer\n            : Theme.of(context).colorScheme.outline,\n        style: ButtonStyle(\n          padding: MaterialStateProperty.all(EdgeInsets.zero),\n          backgroundColor: MaterialStateProperty.resolveWith((states) {\n            return selected\n                ? Theme.of(context).colorScheme.secondaryContainer\n                : null;\n          }),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply_new/view.dart",
    "content": "import 'dart:async';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/dynamics.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/models/video/reply/emote.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/pages/emote/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\n\nimport 'toolbar_icon_button.dart';\n\nclass VideoReplyNewDialog extends StatefulWidget {\n  final int? oid;\n  final int? root;\n  final int? parent;\n  final ReplyType? replyType;\n  final ReplyItemModel? replyItem;\n\n  const VideoReplyNewDialog({\n    super.key,\n    this.oid,\n    this.root,\n    this.parent,\n    this.replyType,\n    this.replyItem,\n  });\n\n  @override\n  State<VideoReplyNewDialog> createState() => _VideoReplyNewDialogState();\n}\n\nclass _VideoReplyNewDialogState extends State<VideoReplyNewDialog>\n    with WidgetsBindingObserver {\n  final TextEditingController _replyContentController = TextEditingController();\n  final FocusNode replyContentFocusNode = FocusNode();\n  final GlobalKey _formKey = GlobalKey<FormState>();\n  late double emoteHeight = 0.0;\n  double keyboardHeight = 0.0; // 键盘高度\n  final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间\n  String toolbarType = 'input';\n  RxBool isForward = false.obs;\n  RxBool showForward = false.obs;\n  RxString message = ''.obs;\n\n  @override\n  void initState() {\n    super.initState();\n    // 监听输入框聚焦\n    // replyContentFocusNode.addListener(_onFocus);\n    // 界面观察者 必须\n    WidgetsBinding.instance.addObserver(this);\n    // 自动聚焦\n    _autoFocus();\n    // 监听聚焦状态\n    _focuslistener();\n    final String routePath = Get.currentRoute;\n    if (routePath.startsWith('/video')) {\n      showForward.value = true;\n    }\n  }\n\n  _autoFocus() async {\n    await Future.delayed(const Duration(milliseconds: 300));\n    if (context.mounted) {\n      FocusScope.of(context).requestFocus(replyContentFocusNode);\n    }\n  }\n\n  _focuslistener() {\n    replyContentFocusNode.addListener(() {\n      if (replyContentFocusNode.hasFocus) {\n        setState(() {\n          toolbarType = 'input';\n        });\n      }\n    });\n  }\n\n  Future submitReplyAdd() async {\n    feedBack();\n    // String message = _replyContentController.text;\n    var result = await VideoHttp.replyAdd(\n      type: widget.replyType ?? ReplyType.video,\n      oid: widget.oid!,\n      root: widget.root!,\n      parent: widget.parent!,\n      message: widget.replyItem != null && widget.replyItem!.root != 0\n          ? ' 回复 @${widget.replyItem!.member!.uname!} : ${message.value}'\n          : message.value,\n    );\n    if (result['status']) {\n      SmartDialog.showToast(result['data']['success_toast']);\n      Get.back(result: {\n        'data': ReplyItemModel.fromJson(result['data']['reply'], ''),\n      });\n\n      /// 投稿、番剧页面\n      if (isForward.value) {\n        await DynamicsHttp.dynamicCreate(\n          mid: 0,\n          rawText: message.value,\n          oid: widget.oid!,\n          scene: 5,\n        );\n      }\n    } else {\n      SmartDialog.showToast(result['msg']);\n    }\n  }\n\n  void onChooseEmote(PackageItem package, Emote emote) {\n    final int cursorPosition = _replyContentController.selection.baseOffset;\n    final String currentText = _replyContentController.text;\n    final String newText = currentText.substring(0, cursorPosition) +\n        emote.text! +\n        currentText.substring(cursorPosition);\n    message.value = newText;\n    _replyContentController.value = TextEditingValue(\n      text: newText,\n      selection:\n          TextSelection.collapsed(offset: cursorPosition + emote.text!.length),\n    );\n  }\n\n  @override\n  void didChangeMetrics() {\n    super.didChangeMetrics();\n    final String routePath = Get.currentRoute;\n    if (mounted &&\n        (routePath.startsWith('/video') ||\n            routePath.startsWith('/dynamicDetail'))) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        // 键盘高度\n        final viewInsets = EdgeInsets.fromViewPadding(\n            View.of(context).viewInsets, View.of(context).devicePixelRatio);\n        _debouncer.run(() {\n          if (mounted) {\n            if (keyboardHeight == 0 && emoteHeight == 0) {\n              setState(() {\n                emoteHeight = keyboardHeight =\n                    keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;\n              });\n            }\n          }\n        });\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    _replyContentController.dispose();\n    replyContentFocusNode.removeListener(() {});\n    replyContentFocusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    double _keyboardHeight = EdgeInsets.fromViewPadding(\n            View.of(context).viewInsets, View.of(context).devicePixelRatio)\n        .bottom;\n    return Container(\n      clipBehavior: Clip.hardEdge,\n      decoration: BoxDecoration(\n        borderRadius: const BorderRadius.only(\n          topLeft: Radius.circular(12),\n          topRight: Radius.circular(12),\n        ),\n        color: Theme.of(context).colorScheme.surface,\n      ),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          ConstrainedBox(\n            constraints: const BoxConstraints(\n              maxHeight: 200,\n              minHeight: 120,\n            ),\n            child: Container(\n              padding: const EdgeInsets.only(\n                  top: 12, right: 15, left: 15, bottom: 10),\n              child: SingleChildScrollView(\n                child: Form(\n                  key: _formKey,\n                  autovalidateMode: AutovalidateMode.onUserInteraction,\n                  child: TextField(\n                    controller: _replyContentController,\n                    minLines: 3,\n                    maxLines: null,\n                    autofocus: false,\n                    focusNode: replyContentFocusNode,\n                    decoration: const InputDecoration(\n                        hintText: \"输入回复内容\",\n                        border: InputBorder.none,\n                        hintStyle: TextStyle(\n                          fontSize: 14,\n                        )),\n                    style: Theme.of(context).textTheme.bodyLarge,\n                    onChanged: (text) {\n                      message.value = text;\n                    },\n                  ),\n                ),\n              ),\n            ),\n          ),\n          Divider(\n            height: 1,\n            color: Theme.of(context).dividerColor.withOpacity(0.1),\n          ),\n          Container(\n            height: 52,\n            padding: const EdgeInsets.only(\n              left: 12,\n              right: 12,\n            ),\n            margin: EdgeInsets.only(\n              bottom: toolbarType == 'input' && keyboardHeight == 0.0\n                  ? MediaQuery.of(context).padding.bottom\n                  : 0,\n            ),\n            child: Row(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                ToolbarIconButton(\n                  onPressed: () {\n                    if (toolbarType == 'emote') {\n                      setState(() {\n                        toolbarType = 'input';\n                      });\n                    }\n                    FocusScope.of(context).requestFocus(replyContentFocusNode);\n                  },\n                  icon: const Icon(Icons.keyboard, size: 22),\n                  toolbarType: toolbarType,\n                  selected: toolbarType == 'input',\n                ),\n                const SizedBox(width: 20),\n                ToolbarIconButton(\n                  onPressed: () {\n                    if (toolbarType == 'input') {\n                      setState(() {\n                        toolbarType = 'emote';\n                      });\n                    }\n                    FocusScope.of(context).unfocus();\n                  },\n                  icon: const Icon(Icons.emoji_emotions, size: 22),\n                  toolbarType: toolbarType,\n                  selected: toolbarType == 'emote',\n                ),\n                const SizedBox(width: 6),\n                Obx(\n                  () => showForward.value\n                      ? TextButton.icon(\n                          onPressed: () {\n                            isForward.value = !isForward.value;\n                          },\n                          icon: Icon(\n                              isForward.value\n                                  ? Icons.check_box\n                                  : Icons.check_box_outline_blank,\n                              size: 22),\n                          label: const Text('转发到动态'),\n                          style: ButtonStyle(\n                            foregroundColor: MaterialStateProperty.all(\n                              isForward.value\n                                  ? Theme.of(context).colorScheme.primary\n                                  : Theme.of(context).colorScheme.outline,\n                            ),\n                          ),\n                        )\n                      : const SizedBox(),\n                ),\n                const Spacer(),\n                SizedBox(\n                  height: 36,\n                  child: Obx(\n                    () => FilledButton(\n                      onPressed: message.isNotEmpty ? submitReplyAdd : null,\n                      child: const Text('发送'),\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n          AnimatedSize(\n            curve: Curves.easeInOut,\n            duration: const Duration(milliseconds: 300),\n            child: SizedBox(\n              width: double.infinity,\n              height: toolbarType == 'input'\n                  ? (_keyboardHeight > keyboardHeight\n                      ? _keyboardHeight\n                      : keyboardHeight)\n                  : emoteHeight,\n              child: EmotePanel(\n                onChoose: (package, emote) => onChooseEmote(package, emote),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\ntypedef DebounceCallback = void Function();\n\nclass Debouncer {\n  DebounceCallback? callback;\n  final int? milliseconds;\n  Timer? _timer;\n\n  Debouncer({this.milliseconds});\n\n  run(DebounceCallback callback) {\n    if (_timer != null) {\n      _timer!.cancel();\n    }\n    _timer = Timer(Duration(milliseconds: milliseconds!), () {\n      callback();\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply_reply/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/reply.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\n\nclass VideoReplyReplyController extends GetxController {\n  VideoReplyReplyController(this.aid, this.rpid, this.replyType);\n  final ScrollController scrollController = ScrollController();\n  // 视频aid 请求时使用的oid\n  int? aid;\n  // rpid 请求楼中楼回复\n  String? rpid;\n  ReplyType replyType = ReplyType.video;\n  RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;\n  // 当前页\n  int currentPage = 0;\n  bool isLoadingMore = false;\n  RxString noMore = ''.obs;\n  // 当前回复的回复\n  ReplyItemModel? currentReplyItem;\n\n  @override\n  void onInit() {\n    super.onInit();\n    currentPage = 0;\n  }\n\n  Future queryReplyList({type = 'init', currentReply}) async {\n    if (type == 'init') {\n      currentPage = 0;\n    }\n    if (isLoadingMore) {\n      return;\n    }\n    isLoadingMore = true;\n    final res = await ReplyHttp.replyReplyList(\n      oid: aid!,\n      root: rpid!,\n      pageNum: currentPage + 1,\n      type: replyType.index,\n    );\n    if (res['status']) {\n      final List<ReplyItemModel> replies = res['data'].replies;\n      if (replies.isNotEmpty) {\n        noMore.value = '加载中...';\n        if (replies.length == res['data'].page.count) {\n          noMore.value = '没有更多了';\n        }\n        currentPage++;\n      } else {\n        // 未登录状态replies可能返回null\n        noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';\n      }\n      if (type == 'init') {\n        replyList.value = replies;\n      } else {\n        // 每次回复之后，翻页请求有且只有相同的一条回复数据\n        if (replies.length == 1 && replies.last.rpid == replyList.last.rpid) {\n          return;\n        }\n        replyList.addAll(replies);\n        // res['data'].replies.addAll(replyList);\n      }\n    }\n    if (replyList.isNotEmpty && currentReply != null) {\n      int indexToRemove =\n          replyList.indexWhere((item) => currentReply.rpid == item.rpid);\n      // 如果找到了指定ID的项，则移除\n      if (indexToRemove != -1) {\n        replyList.removeAt(indexToRemove);\n      }\n      if (currentPage == 1 && type == 'init') {\n        replyList.insert(0, currentReply);\n      }\n    }\n    isLoadingMore = false;\n    return res;\n  }\n\n  @override\n  void onClose() {\n    currentPage = 0;\n    super.onClose();\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/reply_reply/index.dart",
    "content": "library video_reply_reply_panel;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/video/detail/reply_reply/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/skeleton/video_reply.dart';\nimport 'package:pilipala/common/widgets/http_error.dart';\nimport 'package:pilipala/models/common/reply_type.dart';\nimport 'package:pilipala/models/video/reply/item.dart';\nimport 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nimport 'controller.dart';\n\nclass VideoReplyReplyPanel extends StatefulWidget {\n  const VideoReplyReplyPanel({\n    this.oid,\n    this.rpid,\n    this.closePanel,\n    this.firstFloor,\n    this.source,\n    this.replyType,\n    this.sheetHeight,\n    this.currentReply,\n    this.loadMore = true,\n    super.key,\n  });\n  final int? oid;\n  final int? rpid;\n  final Function? closePanel;\n  final ReplyItemModel? firstFloor;\n  final String? source;\n  final ReplyType? replyType;\n  final double? sheetHeight;\n  final dynamic currentReply;\n  final bool loadMore;\n\n  @override\n  State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();\n}\n\nclass _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {\n  late VideoReplyReplyController _videoReplyReplyController;\n  late AnimationController replyAnimationCtl;\n  final Box<dynamic> localCache = GStrorage.localCache;\n  Future? _futureBuilderFuture;\n  late ScrollController scrollController;\n\n  @override\n  void initState() {\n    _videoReplyReplyController = Get.put(\n        VideoReplyReplyController(\n            widget.oid, widget.rpid.toString(), widget.replyType!),\n        tag: widget.rpid.toString());\n    super.initState();\n\n    // 上拉加载更多\n    scrollController = _videoReplyReplyController.scrollController;\n    scrollController.addListener(\n      () {\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 300) {\n          EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),\n              () {\n            _videoReplyReplyController.queryReplyList(type: 'onLoad');\n          });\n        }\n      },\n    );\n\n    _futureBuilderFuture = _videoReplyReplyController.queryReplyList(\n      currentReply: widget.currentReply,\n    );\n  }\n\n  void replyReply(replyItem) {}\n\n  @override\n  void dispose() {\n    // scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: widget.source == 'videoDetail' ? widget.sheetHeight : null,\n      color: Theme.of(context).colorScheme.surface,\n      child: Column(\n        children: [\n          if (widget.source == 'videoDetail')\n            AppBar(\n              toolbarHeight: 45,\n              automaticallyImplyLeading: false,\n              centerTitle: false,\n              title: Text(\n                '评论详情',\n                style: Theme.of(context).textTheme.titleSmall,\n              ),\n              actions: [\n                IconButton(\n                  icon: const Icon(Icons.close, size: 20),\n                  onPressed: () {\n                    _videoReplyReplyController.currentPage = 0;\n                    widget.closePanel?.call;\n                    Navigator.pop(context);\n                  },\n                ),\n                const SizedBox(width: 14),\n              ],\n            ),\n          Expanded(\n            child: RefreshIndicator(\n              onRefresh: () async {\n                setState(() {});\n                _videoReplyReplyController.currentPage = 0;\n                return await _videoReplyReplyController.queryReplyList(\n                  currentReply: widget.currentReply,\n                );\n              },\n              child: CustomScrollView(\n                controller: _videoReplyReplyController.scrollController,\n                slivers: <Widget>[\n                  if (widget.firstFloor != null) ...[\n                    // const SliverToBoxAdapter(child: SizedBox(height: 10)),\n                    SliverToBoxAdapter(\n                      child: ReplyItem(\n                        replyItem: widget.firstFloor,\n                        replyLevel: '2',\n                        showReplyRow: false,\n                        addReply: (replyItem) {\n                          _videoReplyReplyController.replyList.add(replyItem);\n                        },\n                        replyType: widget.replyType,\n                        replyReply: (replyItem) => replyReply(replyItem),\n                      ),\n                    ),\n                    SliverToBoxAdapter(\n                      child: Divider(\n                        height: 20,\n                        color: Theme.of(context).dividerColor.withOpacity(0.1),\n                        thickness: 6,\n                      ),\n                    ),\n                  ],\n                  widget.loadMore\n                      ? FutureBuilder(\n                          future: _futureBuilderFuture,\n                          builder: (BuildContext context, snapshot) {\n                            if (snapshot.connectionState ==\n                                ConnectionState.done) {\n                              Map? data = snapshot.data;\n                              if (data != null && data['status']) {\n                                // 请求成功\n                                return Obx(\n                                  () => SliverList(\n                                    delegate: SliverChildBuilderDelegate(\n                                      (BuildContext context, int index) {\n                                        if (index ==\n                                            _videoReplyReplyController\n                                                .replyList.length) {\n                                          return Container(\n                                            padding: EdgeInsets.only(\n                                                bottom: MediaQuery.of(context)\n                                                    .padding\n                                                    .bottom),\n                                            height: MediaQuery.of(context)\n                                                    .padding\n                                                    .bottom +\n                                                100,\n                                            child: Center(\n                                              child: Obx(\n                                                () => Text(\n                                                  _videoReplyReplyController\n                                                      .noMore.value,\n                                                  style: TextStyle(\n                                                    fontSize: 12,\n                                                    color: Theme.of(context)\n                                                        .colorScheme\n                                                        .outline,\n                                                  ),\n                                                ),\n                                              ),\n                                            ),\n                                          );\n                                        } else {\n                                          return ReplyItem(\n                                            replyItem:\n                                                _videoReplyReplyController\n                                                    .replyList[index],\n                                            replyLevel: '2',\n                                            showReplyRow: false,\n                                            addReply: (replyItem) {\n                                              _videoReplyReplyController\n                                                  .replyList\n                                                  .add(replyItem);\n                                            },\n                                            replyType: widget.replyType,\n                                            replyReply: (replyItem) =>\n                                                replyReply(replyItem),\n                                          );\n                                        }\n                                      },\n                                      childCount: _videoReplyReplyController\n                                              .replyList.length +\n                                          1,\n                                    ),\n                                  ),\n                                );\n                              } else {\n                                // 请求错误\n                                return HttpError(\n                                  errMsg: data?['msg'] ?? '请求错误',\n                                  fn: () => setState(() {}),\n                                );\n                              }\n                            } else {\n                              // 骨架屏\n                              return SliverList(\n                                delegate: SliverChildBuilderDelegate(\n                                    (BuildContext context, int index) {\n                                  return const VideoReplySkeleton();\n                                }, childCount: 8),\n                              );\n                            }\n                          },\n                        )\n                      : SliverToBoxAdapter(\n                          child: SizedBox(\n                            height: 200,\n                            child: Center(\n                              child: Text(\n                                '还没有评论',\n                                style: TextStyle(\n                                  fontSize: 12,\n                                  color: Theme.of(context).colorScheme.outline,\n                                ),\n                              ),\n                            ),\n                          ),\n                        )\n                ],\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/view.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';\nimport 'package:floating/floating.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:flutter_svg/svg.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hive/hive.dart';\nimport 'package:lottie/lottie.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/pages/bangumi/introduction/index.dart';\nimport 'package:pilipala/pages/danmaku/view.dart';\nimport 'package:pilipala/pages/main/index.dart';\nimport 'package:pilipala/pages/video/detail/reply/index.dart';\nimport 'package:pilipala/pages/video/detail/controller.dart';\nimport 'package:pilipala/pages/video/detail/introduction/index.dart';\nimport 'package:pilipala/pages/video/detail/related/index.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/plugin/pl_player/models/play_repeat.dart';\nimport 'package:pilipala/services/service_locator.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:status_bar_control/status_bar_control.dart';\n\nimport '../../../plugin/pl_player/models/bottom_control_type.dart';\nimport '../../../services/shutdown_timer_service.dart';\nimport 'widgets/app_bar.dart';\nimport 'widgets/header_control.dart';\n\nclass VideoDetailPage extends StatefulWidget {\n  const VideoDetailPage({Key? key}) : super(key: key);\n\n  @override\n  State<VideoDetailPage> createState() => _VideoDetailPageState();\n  static final RouteObserver<PageRoute> routeObserver =\n      RouteObserver<PageRoute>();\n}\n\nclass _VideoDetailPageState extends State<VideoDetailPage>\n    with TickerProviderStateMixin, RouteAware, WidgetsBindingObserver {\n  late VideoDetailController vdCtr;\n  PlPlayerController? plPlayerController;\n  final ScrollController _extendNestCtr = ScrollController();\n  late StreamController<double> appbarStream;\n  late VideoIntroController videoIntroController;\n  late BangumiIntroController bangumiIntroController;\n  late String heroTag;\n\n  Rx<PlayerStatus> playerStatus = PlayerStatus.playing.obs;\n  double doubleOffset = 0;\n\n  final Box<dynamic> localCache = GStrorage.localCache;\n  final Box<dynamic> setting = GStrorage.setting;\n  late double statusBarHeight;\n  final double videoHeight = Get.size.width * 9 / 16;\n  late Future _futureBuilderFuture;\n  // 自动退出全屏\n  late bool autoExitFullcreen;\n  late bool autoPlayEnable;\n  late bool autoPiP;\n  late Floating floating;\n  RxBool isShowing = true.obs;\n  // 生命周期监听\n  late final AppLifecycleListener _lifecycleListener;\n  late double statusHeight;\n\n  @override\n  void initState() {\n    super.initState();\n    getStatusHeight();\n    heroTag = Get.arguments['heroTag'];\n    vdCtr = Get.put(VideoDetailController(), tag: heroTag);\n    vdCtr.sheetHeight.value = localCache.get('sheetHeight');\n    videoIntroController = Get.put(\n        VideoIntroController(bvid: Get.parameters['bvid']!),\n        tag: heroTag);\n    videoIntroController.videoDetail.listen((value) {\n      videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);\n    });\n    if (vdCtr.videoType == SearchType.media_bangumi) {\n      bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);\n      bangumiIntroController.bangumiDetail.listen((value) {\n        videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);\n      });\n      vdCtr.cid.listen((p0) {\n        videoPlayerServiceHandler.onVideoDetailChange(\n            bangumiIntroController.bangumiDetail.value, p0);\n      });\n    }\n    statusBarHeight = localCache.get('statusBarHeight');\n    autoExitFullcreen =\n        setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);\n    autoPlayEnable =\n        setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);\n    autoPiP = setting.get(SettingBoxKey.autoPiP, defaultValue: false);\n\n    videoSourceInit();\n    appbarStreamListen();\n    fullScreenStatusListener();\n    if (Platform.isAndroid) {\n      floating = vdCtr.floating!;\n    }\n    WidgetsBinding.instance.addObserver(this);\n    lifecycleListener();\n  }\n\n  // 获取视频资源，初始化播放器\n  Future<void> videoSourceInit() async {\n    _futureBuilderFuture = vdCtr.queryVideoUrl();\n    if (vdCtr.autoPlay.value) {\n      plPlayerController = vdCtr.plPlayerController;\n      plPlayerController!.addStatusLister(playerListener);\n    }\n  }\n\n  // 流\n  appbarStreamListen() {\n    appbarStream = StreamController<double>.broadcast();\n    _extendNestCtr.addListener(\n      () {\n        final double offset = _extendNestCtr.position.pixels;\n        vdCtr.sheetHeight.value =\n            Get.size.height - videoHeight - statusBarHeight + offset;\n        appbarStream.add(offset);\n      },\n    );\n  }\n\n  // 播放器状态监听\n  void playerListener(PlayerStatus status) async {\n    playerStatus.value = status;\n    autoEnterPip(status: status);\n    if (status == PlayerStatus.completed) {\n      // 结束播放退出全屏\n      if (autoExitFullcreen) {\n        plPlayerController!.triggerFullScreen(status: false);\n      }\n      shutdownTimerService.handleWaitingFinished();\n\n      /// 顺序播放 列表循环\n      if (plPlayerController!.playRepeat != PlayRepeat.pause &&\n          plPlayerController!.playRepeat != PlayRepeat.singleCycle) {\n        if (vdCtr.videoType == SearchType.video) {\n          videoIntroController.nextPlay();\n        }\n        if (vdCtr.videoType == SearchType.media_bangumi) {\n          bangumiIntroController.nextPlay();\n        }\n      }\n\n      /// 单个循环\n      if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) {\n        plPlayerController!.seekTo(Duration.zero);\n        plPlayerController!.play();\n      }\n      // 播放完展示控制栏\n      try {\n        PiPStatus currentStatus = await vdCtr.floating!.pipStatus;\n        if (currentStatus == PiPStatus.disabled) {\n          plPlayerController!.onLockControl(false);\n        }\n      } catch (_) {}\n    }\n    if (Platform.isAndroid) {\n      floating.toggleAutoPip(\n          autoEnter: status == PlayerStatus.playing && autoPiP);\n    }\n  }\n\n  // 继续播放或重新播放\n  void continuePlay() async {\n    await _extendNestCtr.animateTo(0,\n        duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);\n    plPlayerController!.play();\n  }\n\n  /// 未开启自动播放时触发播放\n  Future<void> handlePlay() async {\n    await vdCtr.playerInit(autoplay: true);\n    plPlayerController = vdCtr.plPlayerController;\n    plPlayerController!.addStatusLister(playerListener);\n    vdCtr.autoPlay.value = true;\n    vdCtr.isShowCover.value = false;\n    isShowing.value = true;\n    autoEnterPip(status: PlayerStatus.playing);\n  }\n\n  void fullScreenStatusListener() {\n    plPlayerController?.isFullScreen.listen((bool isFullScreen) {\n      if (isFullScreen) {\n        vdCtr.hiddenReplyReplyPanel();\n        if (vdCtr.videoType == SearchType.video) {\n          videoIntroController.hiddenEpisodeBottomSheet();\n          if (videoIntroController.videoDetail.value.ugcSeason != null ||\n              (videoIntroController.videoDetail.value.pages != null &&\n                  videoIntroController.videoDetail.value.pages!.length > 1)) {\n            vdCtr.bottomList.insert(3, BottomControlType.episode);\n          }\n        }\n        if (vdCtr.videoType == SearchType.media_bangumi) {\n          bangumiIntroController.hiddenEpisodeBottomSheet();\n          if (bangumiIntroController.bangumiDetail.value.episodes != null &&\n              bangumiIntroController.bangumiDetail.value.episodes!.length > 1) {\n            vdCtr.bottomList.insert(3, BottomControlType.episode);\n          }\n        }\n      } else {\n        if (vdCtr.bottomList.contains(BottomControlType.episode)) {\n          vdCtr.bottomList.removeAt(3);\n        }\n      }\n      vdCtr.toggeleWatchLaterVisible(!isFullScreen);\n    });\n  }\n\n  getStatusHeight() async {\n    statusHeight = await StatusBarControl.getHeight;\n  }\n\n  @override\n  void dispose() {\n    shutdownTimerService.handleWaitingFinished();\n    if (plPlayerController != null) {\n      plPlayerController!.removeStatusLister(playerListener);\n      plPlayerController!.dispose();\n    }\n    if (vdCtr.floating != null) {\n      vdCtr.floating!.dispose();\n    }\n    videoPlayerServiceHandler.onVideoDetailDispose();\n    if (Platform.isAndroid) {\n      floating.toggleAutoPip(autoEnter: false);\n      floating.dispose();\n    }\n    appbarStream.close();\n    WidgetsBinding.instance.removeObserver(this);\n    _lifecycleListener.dispose();\n    super.dispose();\n  }\n\n  @override\n  // 离开当前页面时\n  void didPushNext() async {\n    final MainController mainController = Get.find<MainController>();\n    if (mainController.imgPreviewStatus) {\n      return;\n    }\n\n    /// 开启\n    if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)\n        as bool) {\n      vdCtr.brightness = plPlayerController!.brightness.value;\n    }\n    if (plPlayerController != null) {\n      vdCtr.defaultST = plPlayerController!.position.value;\n      videoIntroController.isPaused = true;\n      plPlayerController!.removeStatusLister(playerListener);\n      plPlayerController!.pause();\n      vdCtr.clearSubtitleContent();\n    }\n    isShowing.value = false;\n    super.didPushNext();\n  }\n\n  @override\n  // 返回当前页面时\n  void didPopNext() async {\n    final MainController mainController = Get.find<MainController>();\n    if (mainController.imgPreviewStatus) {\n      return;\n    }\n\n    if (plPlayerController != null &&\n        plPlayerController!.videoPlayerController != null) {\n      vdCtr.setSubtitleContent();\n      isShowing.value = true;\n    }\n    vdCtr.isFirstTime = false;\n    final bool autoplay = autoPlayEnable;\n    vdCtr.playerInit();\n\n    /// 未开启自动播放时，未播放跳转下一页返回/播放后跳转下一页返回\n    vdCtr.autoPlay.value = !vdCtr.isShowCover.value;\n    videoIntroController.isPaused = false;\n    if (_extendNestCtr.position.pixels == 0 && autoplay) {\n      await Future.delayed(const Duration(milliseconds: 300));\n      plPlayerController?.seekTo(vdCtr.defaultST);\n      plPlayerController?.play();\n    }\n    plPlayerController?.addStatusLister(playerListener);\n    appbarStream.add(0);\n    super.didPopNext();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    VideoDetailPage.routeObserver\n        .subscribe(this, ModalRoute.of(context)! as PageRoute);\n  }\n\n  void autoEnterPip({PlayerStatus? status}) {\n    final String routePath = Get.currentRoute;\n    if (autoPiP && routePath.startsWith('/video')) {\n      floating.toggleAutoPip(\n        autoEnter: autoPiP && status == PlayerStatus.playing,\n      );\n    }\n  }\n\n  // 生命周期监听\n  void lifecycleListener() {\n    _lifecycleListener = AppLifecycleListener(\n      // onResume: () => _handleTransition('resume'),\n      // 后台\n      // onInactive: () => _handleTransition('inactive'),\n      // 在Android和iOS端不生效\n      // onHide: () => _handleTransition('hide'),\n      onShow: () => _handleTransition('show'),\n      onPause: () => _handleTransition('pause'),\n      onRestart: () => _handleTransition('restart'),\n      onDetach: () => _handleTransition('detach'),\n    );\n  }\n\n  void _handleTransition(String name) {\n    switch (name) {\n      case 'show' || 'restart':\n        plPlayerController?.danmakuController?.clear();\n        break;\n      case 'pause':\n        if (autoPiP) {\n          vdCtr.hiddenReplyReplyPanel();\n          if (vdCtr.videoType == SearchType.video) {\n            videoIntroController.hiddenEpisodeBottomSheet();\n          }\n          if (vdCtr.videoType == SearchType.media_bangumi) {\n            bangumiIntroController.hiddenEpisodeBottomSheet();\n          }\n        }\n        break;\n    }\n  }\n\n  /// 手动播放\n  Widget handlePlayPanel() {\n    return Stack(\n      children: [\n        GestureDetector(\n          onTap: handlePlay,\n          child: Obx(\n            () => NetworkImgLayer(\n              src: vdCtr.cover.value,\n              width: Get.width,\n              height: videoHeight,\n              type: 'emote',\n            ),\n          ),\n        ),\n        buildCustomAppBar(),\n        Positioned(\n          right: 12,\n          bottom: 10,\n          child: GestureDetector(\n            onTap: handlePlay,\n            child: Image.asset(\n              'assets/images/play.png',\n              width: 60,\n              height: 60,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  /// tabbar\n  Widget tabbarBuild() {\n    return Container(\n      width: double.infinity,\n      height: 45,\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            width: 1,\n            color: Theme.of(context).dividerColor.withOpacity(0.1),\n          ),\n        ),\n      ),\n      child: Material(\n        child: Row(\n          children: [\n            Expanded(\n              child: Obx(\n                () => TabBar(\n                  padding: EdgeInsets.zero,\n                  controller: vdCtr.tabCtr,\n                  labelStyle: const TextStyle(fontSize: 13),\n                  labelPadding: const EdgeInsets.symmetric(horizontal: 10.0),\n                  dividerColor: Colors.transparent,\n                  tabs:\n                      vdCtr.tabs.map((String name) => Tab(text: name)).toList(),\n                  onTap: (index) => vdCtr.onTapTabbar(index),\n                ),\n              ),\n            ),\n            Flexible(\n              flex: 1,\n              child: Center(\n                child: Row(\n                  mainAxisAlignment: MainAxisAlignment.end,\n                  children: [\n                    Obx(() => AnimatedOpacity(\n                          opacity: playerStatus.value != PlayerStatus.playing\n                              ? 1\n                              : 0,\n                          duration: const Duration(milliseconds: 100),\n                          child: const Icon(\n                            Icons.drag_handle_rounded,\n                            size: 20,\n                            color: Colors.grey,\n                          ),\n                        )),\n                    const SizedBox(width: 8),\n                    SizedBox(\n                      height: 32,\n                      child: TextButton(\n                        style: ButtonStyle(\n                          padding: MaterialStateProperty.all(EdgeInsets.zero),\n                        ),\n                        onPressed: () => vdCtr.showShootDanmakuSheet(),\n                        child:\n                            const Text('发弹幕', style: TextStyle(fontSize: 12)),\n                      ),\n                    ),\n                    SizedBox(\n                      width: 38,\n                      height: 38,\n                      child: Obx(\n                        () => !vdCtr.isShowCover.value\n                            ? IconButton(\n                                onPressed: () {\n                                  plPlayerController?.isOpenDanmu.value =\n                                      !(plPlayerController?.isOpenDanmu.value ??\n                                          false);\n                                },\n                                icon: !(plPlayerController?.isOpenDanmu.value ??\n                                        false)\n                                    ? SvgPicture.asset(\n                                        'assets/images/video/danmu_close.svg',\n                                        // ignore: deprecated_member_use\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .outline,\n                                      )\n                                    : SvgPicture.asset(\n                                        'assets/images/video/danmu_open.svg',\n                                        // ignore: deprecated_member_use\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .primary,\n                                      ),\n                              )\n                            : IconButton(\n                                icon: SvgPicture.asset(\n                                  'assets/images/video/danmu_close.svg',\n                                  // ignore: deprecated_member_use\n                                  color: Theme.of(context).colorScheme.outline,\n                                ),\n                                onPressed: () {},\n                              ),\n                      ),\n                    ),\n                    const SizedBox(width: 18),\n                  ],\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final sizeContext = MediaQuery.sizeOf(context);\n    final _context = MediaQuery.of(context);\n    late double defaultVideoHeight = sizeContext.width * 9 / 16;\n    late RxDouble videoHeight = defaultVideoHeight.obs;\n    final double pinnedHeaderHeight =\n        statusBarHeight + kToolbarHeight + videoHeight.value;\n    // ignore: no_leading_underscores_for_local_identifiers\n\n    // 竖屏\n    final bool isPortrait = _context.orientation == Orientation.portrait;\n    // 横屏\n    final bool isLandscape = _context.orientation == Orientation.landscape;\n    final Rx<bool> isFullScreen = plPlayerController?.isFullScreen ?? false.obs;\n    // 全屏时高度撑满\n    if (isLandscape || isFullScreen.value == true) {\n      videoHeight.value = Get.size.height;\n      enterFullScreen();\n    } else {\n      videoHeight.value = defaultVideoHeight;\n      exitFullScreen();\n    }\n\n    Widget buildLoadingWidget() {\n      return Center(child: Lottie.asset('assets/loading.json', width: 200));\n    }\n\n    Widget buildVideoPlayerWidget(AsyncSnapshot snapshot) {\n      return Obx(() => !vdCtr.autoPlay.value\n          ? const SizedBox()\n          : PLVideoPlayer(\n              controller: plPlayerController!,\n              headerControl: vdCtr.headerControl,\n              danmuWidget: PlDanmaku(\n                key: Key(vdCtr.danmakuCid.value.toString()),\n                cid: vdCtr.danmakuCid.value,\n                playerController: plPlayerController!,\n              ),\n              bottomList: vdCtr.bottomList,\n              showEposideCb: () => vdCtr.videoType == SearchType.video\n                  ? videoIntroController.showEposideHandler()\n                  : bangumiIntroController.showEposideHandler(),\n              fullScreenCb: (bool status) {\n                videoHeight.value =\n                    status ? Get.size.height : defaultVideoHeight;\n              },\n            ));\n    }\n\n    Widget buildErrorWidget(dynamic error) {\n      return Obx(\n        () => SizedBox(\n          height: videoHeight.value,\n          width: Get.size.width,\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            crossAxisAlignment: CrossAxisAlignment.center,\n            children: [\n              const Text('加载失败', style: TextStyle(color: Colors.white)),\n              Text('$error', style: const TextStyle(color: Colors.white)),\n              const SizedBox(height: 10),\n              IconButton.filled(\n                onPressed: () {\n                  setState(() {\n                    _futureBuilderFuture = vdCtr.queryVideoUrl();\n                  });\n                },\n                icon: const Icon(Icons.refresh),\n              )\n            ],\n          ),\n        ),\n      );\n    }\n\n    /// 播放器面板\n    Widget buildVideoPlayerPanel() {\n      return FutureBuilder(\n        future: _futureBuilderFuture,\n        builder: (BuildContext context, AsyncSnapshot snapshot) {\n          if (snapshot.connectionState == ConnectionState.waiting) {\n            return buildLoadingWidget();\n          } else if (snapshot.connectionState == ConnectionState.done) {\n            if (snapshot.hasData && snapshot.data['status']) {\n              return buildVideoPlayerWidget(snapshot);\n            } else {\n              return buildErrorWidget(snapshot.error);\n            }\n          } else {\n            return buildErrorWidget('未知错误');\n          }\n        },\n      );\n    }\n\n    Widget childWhenDisabled = SafeArea(\n      top: MediaQuery.of(context).orientation == Orientation.portrait &&\n          plPlayerController?.isFullScreen.value == true,\n      bottom: MediaQuery.of(context).orientation == Orientation.portrait &&\n          plPlayerController?.isFullScreen.value == true,\n      left: false,\n      right: false,\n      child: Stack(\n        children: [\n          Scaffold(\n            resizeToAvoidBottomInset: false,\n            key: vdCtr.scaffoldKey,\n            appBar: PreferredSize(\n              preferredSize: const Size.fromHeight(0),\n              child: StreamBuilder(\n                stream: appbarStream.stream.distinct(),\n                initialData: 0,\n                builder: ((context, snapshot) {\n                  return AppBar(\n                    backgroundColor: Colors.black,\n                    elevation: 0,\n                    scrolledUnderElevation: 0,\n                    systemOverlayStyle: Get.isDarkMode\n                        ? SystemUiOverlayStyle.light\n                        : snapshot.data!.toDouble() > kToolbarHeight\n                            ? SystemUiOverlayStyle.dark\n                            : SystemUiOverlayStyle.light,\n                  );\n                }),\n              ),\n            ),\n            body: ExtendedNestedScrollView(\n              controller: _extendNestCtr,\n              headerSliverBuilder:\n                  (BuildContext context2, bool innerBoxIsScrolled) {\n                return <Widget>[\n                  Obx(\n                    () {\n                      final Orientation orientation =\n                          MediaQuery.of(context).orientation;\n                      final bool isFullScreen =\n                          plPlayerController?.isFullScreen.value == true;\n                      final double expandedHeight =\n                          orientation == Orientation.landscape || isFullScreen\n                              ? (MediaQuery.sizeOf(context).height -\n                                  (orientation == Orientation.landscape\n                                      ? 0\n                                      : MediaQuery.of(context).padding.top))\n                              : videoHeight.value;\n                      if (orientation == Orientation.landscape ||\n                          isFullScreen) {\n                        enterFullScreen();\n                      } else {\n                        exitFullScreen();\n                      }\n                      return SliverAppBar(\n                        automaticallyImplyLeading: false,\n                        pinned: true,\n                        elevation: 0,\n                        scrolledUnderElevation: 0,\n                        forceElevated: innerBoxIsScrolled,\n                        expandedHeight: expandedHeight,\n                        backgroundColor: Colors.black,\n                        flexibleSpace: FlexibleSpaceBar(\n                          background: PopScope(\n                            canPop:\n                                plPlayerController?.isFullScreen.value != true,\n                            onPopInvoked: (bool didPop) {\n                              if (plPlayerController?.isFullScreen.value ==\n                                  true) {\n                                plPlayerController!\n                                    .triggerFullScreen(status: false);\n                              }\n                              if (MediaQuery.of(context).orientation ==\n                                  Orientation.landscape) {\n                                verticalScreen();\n                              }\n                            },\n                            child: LayoutBuilder(\n                              builder: (BuildContext context,\n                                  BoxConstraints constraints) {\n                                return Hero(\n                                  tag: heroTag,\n                                  child: Stack(\n                                    children: <Widget>[\n                                      Obx(\n                                        () => isShowing.value\n                                            ? buildVideoPlayerPanel()\n                                            : const SizedBox(),\n                                      ),\n\n                                      /// 关闭自动播放时 手动播放\n                                      Obx(\n                                        () => Visibility(\n                                          visible: !vdCtr.autoPlay.value &&\n                                              vdCtr.isShowCover.value,\n                                          child: Positioned(\n                                            top: 0,\n                                            left: 0,\n                                            right: 0,\n                                            child: handlePlayPanel(),\n                                          ),\n                                        ),\n                                      ),\n                                    ],\n                                  ),\n                                );\n                              },\n                            ),\n                          ),\n                        ),\n                      );\n                    },\n                  ),\n                ];\n              },\n\n              /// 不收回\n              pinnedHeaderSliverHeightBuilder: () {\n                return MediaQuery.of(context).orientation ==\n                            Orientation.landscape ||\n                        plPlayerController?.isFullScreen.value == true\n                    ? MediaQuery.sizeOf(context).height\n                    : playerStatus.value != PlayerStatus.playing\n                        ? kToolbarHeight\n                        : pinnedHeaderHeight;\n              },\n              onlyOneScrollInBody: true,\n              body: Column(\n                children: [\n                  tabbarBuild(),\n                  Expanded(\n                    child: TabBarView(\n                      controller: vdCtr.tabCtr,\n                      children: <Widget>[\n                        Builder(\n                          builder: (BuildContext context) {\n                            return CustomScrollView(\n                              key: const PageStorageKey<String>('简介'),\n                              slivers: <Widget>[\n                                if (vdCtr.videoType == SearchType.video) ...[\n                                  VideoIntroPanel(bvid: vdCtr.bvid),\n                                ] else if (vdCtr.videoType ==\n                                    SearchType.media_bangumi) ...[\n                                  Obx(() =>\n                                      BangumiIntroPanel(cid: vdCtr.cid.value)),\n                                ],\n                                SliverToBoxAdapter(\n                                  child: Divider(\n                                    indent: 12,\n                                    endIndent: 12,\n                                    color: Theme.of(context)\n                                        .dividerColor\n                                        .withOpacity(0.06),\n                                  ),\n                                ),\n                                if (vdCtr.videoType == SearchType.video &&\n                                    vdCtr.enableRelatedVideo)\n                                  const RelatedVideoPanel(),\n                              ],\n                            );\n                          },\n                        ),\n                        Obx(\n                          () => VideoReplyPanel(\n                            bvid: vdCtr.bvid,\n                            oid: vdCtr.oid.value,\n                            onControllerCreated: vdCtr.onControllerCreated,\n                          ),\n                        )\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n\n          /// 重新进入会刷新\n          // 播放完成/暂停播放\n          StreamBuilder(\n            stream: appbarStream.stream.distinct(),\n            initialData: 0,\n            builder: ((context, snapshot) {\n              return ScrollAppBar(\n                snapshot.data!.toDouble(),\n                () => continuePlay(),\n                playerStatus.value,\n                null,\n              );\n            }),\n          ),\n\n          /// 稍后再看列表\n          Obx(\n            () => Visibility(\n              visible: vdCtr.sourceType.value == 'watchLater' ||\n                  vdCtr.sourceType.value == 'fav',\n              child: AnimatedPositioned(\n                duration: const Duration(milliseconds: 400),\n                curve: Curves.easeInOut,\n                left: 12,\n                bottom: vdCtr.isWatchLaterVisible.value\n                    ? MediaQuery.of(context).padding.bottom + 12\n                    : -100,\n                child: Material(\n                  color: Colors.transparent,\n                  child: InkWell(\n                    onTap: () {\n                      vdCtr.toggeleWatchLaterVisible(\n                          !vdCtr.isWatchLaterVisible.value);\n                      vdCtr.showMediaListPanel();\n                    },\n                    borderRadius: const BorderRadius.all(Radius.circular(14)),\n                    child: Container(\n                      width: Get.width - 24,\n                      height: 54,\n                      padding: const EdgeInsets.symmetric(horizontal: 16),\n                      decoration: BoxDecoration(\n                        color: Theme.of(context)\n                            .colorScheme\n                            .secondaryContainer\n                            .withOpacity(0.95),\n                        borderRadius:\n                            const BorderRadius.all(Radius.circular(14)),\n                      ),\n                      child: Row(children: [\n                        const Icon(Icons.playlist_play, size: 24),\n                        const SizedBox(width: 10),\n                        Text(\n                          vdCtr.watchLaterTitle.value,\n                          style: TextStyle(\n                            color: Theme.of(context)\n                                .colorScheme\n                                .onSecondaryContainer,\n                            fontWeight: FontWeight.bold,\n                            letterSpacing: 0.2,\n                          ),\n                        ),\n                        const Spacer(),\n                        const Icon(Icons.keyboard_arrow_up_rounded, size: 26),\n                      ]),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          )\n        ],\n      ),\n    );\n\n    if (Platform.isAndroid) {\n      return PiPSwitcher(\n        childWhenDisabled: childWhenDisabled,\n        childWhenEnabled: buildVideoPlayerPanel(),\n        floating: floating,\n      );\n    } else {\n      return childWhenDisabled;\n    }\n  }\n\n  Widget buildCustomAppBar() {\n    return AppBar(\n      backgroundColor: Colors.transparent, // 使背景透明\n      foregroundColor: Colors.white,\n      elevation: 0,\n      scrolledUnderElevation: 0,\n      primary: false,\n      centerTitle: false,\n      automaticallyImplyLeading: false,\n      titleSpacing: 0,\n      title: Container(\n        height: kToolbarHeight,\n        padding: const EdgeInsets.symmetric(horizontal: 14),\n        decoration: const BoxDecoration(\n            gradient: LinearGradient(\n          begin: Alignment.bottomCenter,\n          end: Alignment.topCenter,\n          colors: <Color>[\n            Colors.transparent,\n            Colors.black54,\n          ],\n          tileMode: TileMode.mirror,\n        )),\n        child: Row(\n          children: [\n            ComBtn(\n              icon: const Icon(FontAwesomeIcons.arrowLeft, size: 15),\n              fuc: () => Get.back(),\n            ),\n            const Spacer(),\n            ComBtn(\n              icon: const Icon(Icons.history_outlined, size: 22),\n              fuc: () async {\n                var res = await UserHttp.toViewLater(bvid: vdCtr.bvid);\n                SmartDialog.showToast(res['msg']);\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/widgets/ai_detail.dart",
    "content": "import 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/video/ai.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nBox localCache = GStrorage.localCache;\nlate double sheetHeight;\n\nclass AiDetail extends StatelessWidget {\n  final ModelResult? modelResult;\n\n  const AiDetail({\n    Key? key,\n    this.modelResult,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    sheetHeight = localCache.get('sheetHeight');\n    return Container(\n      color: Theme.of(context).colorScheme.surface,\n      padding: const EdgeInsets.only(left: 14, right: 14),\n      height: sheetHeight,\n      child: Column(\n        children: [\n          InkWell(\n            onTap: () => Get.back(),\n            child: Container(\n              height: 35,\n              padding: const EdgeInsets.only(bottom: 2),\n              child: Center(\n                child: Container(\n                  width: 32,\n                  height: 3,\n                  decoration: BoxDecoration(\n                    color: Theme.of(context).colorScheme.primary,\n                    borderRadius: const BorderRadius.all(Radius.circular(3)),\n                  ),\n                ),\n              ),\n            ),\n          ),\n          Expanded(\n            child: SingleChildScrollView(\n              child: Column(\n                children: [\n                  if (modelResult!.resultType != 0 &&\n                      modelResult!.summary != '') ...[\n                    SelectableText(\n                      modelResult!.summary!,\n                      style: const TextStyle(\n                        fontSize: 15,\n                        fontWeight: FontWeight.bold,\n                        height: 1.5,\n                      ),\n                    ),\n                    const SizedBox(height: 20),\n                  ],\n                  ListView.builder(\n                    shrinkWrap: true,\n                    physics: const NeverScrollableScrollPhysics(),\n                    itemCount: modelResult!.outline!.length,\n                    itemBuilder: (context, index) {\n                      final outline = modelResult!.outline![index];\n                      return Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          SelectableText(\n                            outline.title!,\n                            style: const TextStyle(\n                              fontSize: 14,\n                              fontWeight: FontWeight.bold,\n                              height: 1.5,\n                            ),\n                          ),\n                          const SizedBox(height: 6),\n                          ListView.builder(\n                            shrinkWrap: true,\n                            physics: const NeverScrollableScrollPhysics(),\n                            itemCount: outline.partOutline!.length,\n                            itemBuilder: (context, i) {\n                              final part = outline.partOutline![i];\n                              return Column(\n                                crossAxisAlignment: CrossAxisAlignment.start,\n                                children: [\n                                  GestureDetector(\n                                    onTap: () {\n                                      try {\n                                        final controller =\n                                            Get.find<VideoDetailController>(\n                                          tag: Get.arguments['heroTag'],\n                                        );\n                                        controller.plPlayerController.seekTo(\n                                          Duration(\n                                            seconds: Utils.duration(\n                                              Utils.tampToSeektime(\n                                                  part.timestamp!),\n                                            ).toInt(),\n                                          ),\n                                        );\n                                      } catch (_) {}\n                                    },\n                                    child: SelectableText.rich(\n                                      TextSpan(\n                                        style: TextStyle(\n                                          fontSize: 13,\n                                          color: Theme.of(context)\n                                              .colorScheme\n                                              .onSurface,\n                                          height: 1.5,\n                                        ),\n                                        children: [\n                                          TextSpan(\n                                            text: Utils.tampToSeektime(\n                                                part.timestamp!),\n                                            style: TextStyle(\n                                              color: Theme.of(context)\n                                                  .colorScheme\n                                                  .primary,\n                                            ),\n                                          ),\n                                          const TextSpan(text: ' '),\n                                          TextSpan(text: part.content!),\n                                        ],\n                                      ),\n                                    ),\n                                  ),\n                                  const SizedBox(height: 20),\n                                ],\n                              );\n                            },\n                          ),\n                        ],\n                      );\n                    },\n                  )\n                ],\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  InlineSpan buildContent(BuildContext context, content) {\n    List descV2 = content.descV2;\n    // type\n    // 1 普通文本\n    // 2 @用户\n    List<TextSpan> spanChilds = List.generate(descV2.length, (index) {\n      final currentDesc = descV2[index];\n      switch (currentDesc.type) {\n        case 1:\n          List<InlineSpan> spanChildren = [];\n          RegExp urlRegExp = RegExp(r'https?://\\S+\\b');\n          Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);\n\n          int previousEndIndex = 0;\n          for (Match match in matches) {\n            if (match.start > previousEndIndex) {\n              spanChildren.add(TextSpan(\n                  text: currentDesc.rawText\n                      .substring(previousEndIndex, match.start)));\n            }\n            spanChildren.add(\n              TextSpan(\n                text: match.group(0),\n                style: TextStyle(\n                    color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色\n                recognizer: TapGestureRecognizer()\n                  ..onTap = () {\n                    // 处理点击事件\n                    try {\n                      Get.toNamed(\n                        '/webview',\n                        parameters: {\n                          'url': match.group(0)!,\n                          'type': 'url',\n                          'pageTitle': match.group(0)!,\n                        },\n                      );\n                    } catch (err) {\n                      SmartDialog.showToast(err.toString());\n                    }\n                  },\n              ),\n            );\n            previousEndIndex = match.end;\n          }\n\n          if (previousEndIndex < currentDesc.rawText.length) {\n            spanChildren.add(TextSpan(\n                text: currentDesc.rawText.substring(previousEndIndex)));\n          }\n\n          TextSpan result = TextSpan(children: spanChildren);\n          return result;\n        case 2:\n          final colorSchemePrimary = Theme.of(context).colorScheme.primary;\n          final heroTag = Utils.makeHeroTag(currentDesc.bizId);\n          return TextSpan(\n            text: '@${currentDesc.rawText}',\n            style: TextStyle(color: colorSchemePrimary),\n            recognizer: TapGestureRecognizer()\n              ..onTap = () {\n                Get.toNamed(\n                  '/member?mid=${currentDesc.bizId}',\n                  arguments: {'face': '', 'heroTag': heroTag},\n                );\n              },\n          );\n        default:\n          return const TextSpan();\n      }\n    });\n    return TextSpan(children: spanChilds);\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/widgets/app_bar.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\n\nclass ScrollAppBar extends StatelessWidget {\n  final double scrollVal;\n  final Function callback;\n  final PlayerStatus playerStatus;\n\n  const ScrollAppBar(\n    this.scrollVal,\n    this.callback,\n    this.playerStatus,\n    Key? key,\n  ) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    final double statusBarHeight = MediaQuery.of(context).padding.top;\n    final videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;\n    double scrollDistance = scrollVal;\n    if (scrollVal > videoHeight - kToolbarHeight) {\n      scrollDistance = videoHeight - kToolbarHeight;\n    }\n    return Positioned(\n      top: -videoHeight + scrollDistance + kToolbarHeight + 0.5,\n      left: 0,\n      right: 0,\n      child: Opacity(\n        opacity: scrollDistance / (videoHeight - kToolbarHeight),\n        child: Container(\n          height: statusBarHeight + kToolbarHeight,\n          color: Theme.of(context).colorScheme.surface,\n          padding: EdgeInsets.only(top: statusBarHeight),\n          child: AppBar(\n            primary: false,\n            elevation: 0,\n            scrolledUnderElevation: 0,\n            centerTitle: true,\n            title: TextButton(\n              onPressed: () => callback(),\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  const Icon(Icons.play_arrow_rounded),\n                  Text(\n                    playerStatus == PlayerStatus.paused\n                        ? '继续播放'\n                        : playerStatus == PlayerStatus.completed\n                            ? '重新播放'\n                            : '播放中',\n                  )\n                ],\n              ),\n            ),\n            // actions: [\n            //   IconButton(\n            //       onPressed: () {},\n            //       icon: const Icon(\n            //         Icons.share,\n            //         size: 20,\n            //       )),\n            //   const SizedBox(width: 12)\n            // ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/widgets/expandable_section.dart",
    "content": "// ignore_for_file: library_private_types_in_public_api\n\nimport 'package:flutter/material.dart';\n\nclass ExpandedSection extends StatefulWidget {\n  final Widget? child;\n  final bool expand;\n  final double begin;\n  final double end;\n\n  const ExpandedSection({\n    super.key,\n    this.expand = false,\n    this.child,\n    this.begin = 0.0,\n    this.end = 1.0,\n  });\n\n  @override\n  _ExpandedSectionState createState() => _ExpandedSectionState();\n}\n\nclass _ExpandedSectionState extends State<ExpandedSection>\n    with SingleTickerProviderStateMixin {\n  late AnimationController expandController;\n  late Animation<double> animation;\n\n  @override\n  void initState() {\n    super.initState();\n    prepareAnimations();\n    _runExpandCheck();\n  }\n\n  void prepareAnimations() {\n    expandController = AnimationController(\n        vsync: this, duration: const Duration(milliseconds: 400));\n    Animation<double> curve = CurvedAnimation(\n      parent: expandController,\n      curve: Curves.linear,\n    );\n    animation = Tween(begin: widget.begin, end: widget.end).animate(curve);\n  }\n\n  void _runExpandCheck() {\n    if (widget.expand) {\n      expandController.forward();\n    } else {\n      expandController.reverse();\n    }\n  }\n\n  @override\n  void didUpdateWidget(ExpandedSection oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (widget.expand != oldWidget.expand) {\n      _runExpandCheck();\n    }\n  }\n\n  @override\n  void dispose() {\n    expandController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizeTransition(\n      axisAlignment: -1.0,\n      sizeFactor: animation,\n      child: widget.child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/widgets/header_control.dart",
    "content": "import 'dart:io';\n\nimport 'package:floating/floating.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:ns_danmaku/ns_danmaku.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/video/play/quality.dart';\nimport 'package:pilipala/models/video/play/url.dart';\nimport 'package:pilipala/pages/dlna/index.dart';\nimport 'package:pilipala/pages/video/detail/index.dart';\nimport 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/plugin/pl_player/models/play_repeat.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:pilipala/services/shutdown_timer_service.dart';\nimport '../../../../http/danmaku.dart';\nimport '../../../../models/common/search_type.dart';\nimport '../../../../models/video_detail_res.dart';\nimport '../introduction/index.dart';\n\nclass HeaderControl extends StatefulWidget implements PreferredSizeWidget {\n  const HeaderControl({\n    this.controller,\n    this.videoDetailCtr,\n    this.floating,\n    this.bvid,\n    this.videoType,\n    this.showSubtitleBtn,\n    super.key,\n  });\n  final PlPlayerController? controller;\n  final VideoDetailController? videoDetailCtr;\n  final Floating? floating;\n  final String? bvid;\n  final SearchType? videoType;\n  final bool? showSubtitleBtn;\n\n  @override\n  State<HeaderControl> createState() => _HeaderControlState();\n\n  @override\n  Size get preferredSize => throw UnimplementedError();\n}\n\nclass _HeaderControlState extends State<HeaderControl> {\n  late PlayUrlModel videoInfo;\n  static const TextStyle subTitleStyle = TextStyle(fontSize: 12);\n  static const TextStyle titleStyle = TextStyle(fontSize: 14);\n  Size get preferredSize => const Size(double.infinity, kToolbarHeight);\n  final Box<dynamic> localCache = GStrorage.localCache;\n  final Box<dynamic> videoStorage = GStrorage.video;\n  late List<double> speedsList;\n  double buttonSpace = 8;\n  RxBool isFullScreen = false.obs;\n  late String heroTag;\n  late VideoIntroController videoIntroController;\n  late VideoDetailData videoDetail;\n\n  @override\n  void initState() {\n    super.initState();\n    videoInfo = widget.videoDetailCtr!.data;\n    speedsList = widget.controller!.speedsList;\n    fullScreenStatusListener();\n    heroTag = Get.arguments['heroTag'];\n    videoIntroController =\n        Get.put(VideoIntroController(bvid: widget.bvid!), tag: heroTag);\n  }\n\n  void fullScreenStatusListener() {\n    widget.videoDetailCtr!.plPlayerController.isFullScreen.listen((bool val) {\n      isFullScreen.value = val;\n\n      /// TODO setState() called after dispose()\n      if (mounted) {\n        setState(() {});\n      }\n    });\n  }\n\n  /// 设置面板\n  void showSettingSheet() {\n    showModalBottomSheet(\n      elevation: 0,\n      context: context,\n      backgroundColor: Colors.transparent,\n      builder: (_) {\n        return Container(\n          width: double.infinity,\n          height: 460,\n          clipBehavior: Clip.hardEdge,\n          decoration: BoxDecoration(\n            color: Theme.of(context).colorScheme.surface,\n            borderRadius: const BorderRadius.all(Radius.circular(12)),\n          ),\n          margin: const EdgeInsets.all(12),\n          child: Column(\n            children: <Widget>[\n              SizedBox(\n                height: 35,\n                child: Center(\n                  child: Container(\n                    width: 32,\n                    height: 3,\n                    decoration: BoxDecoration(\n                        color: Theme.of(context)\n                            .colorScheme\n                            .onSecondaryContainer\n                            .withOpacity(0.5),\n                        borderRadius:\n                            const BorderRadius.all(Radius.circular(3))),\n                  ),\n                ),\n              ),\n              Expanded(\n                  child: Material(\n                child: ListView(\n                  children: [\n                    // ListTile(\n                    //   onTap: () {},\n                    //   dense: true,\n                    //   enabled: false,\n                    //   leading:\n                    //       const Icon(Icons.network_cell_outlined, size: 20),\n                    //   title: Text('省流模式', style: titleStyle),\n                    //   subtitle: Text('低画质 ｜ 减少视频缓存', style: subTitleStyle),\n                    //   trailing: Transform.scale(\n                    //     scale: 0.75,\n                    //     child: Switch(\n                    //       thumbIcon: MaterialStateProperty.resolveWith<Icon?>(\n                    //           (Set<MaterialState> states) {\n                    //         if (states.isNotEmpty &&\n                    //             states.first == MaterialState.selected) {\n                    //           return const Icon(Icons.done);\n                    //         }\n                    //         return null; // All other states will use the default thumbIcon.\n                    //       }),\n                    //       value: false,\n                    //       onChanged: (value) => {},\n                    //     ),\n                    //   ),\n                    // ),\n                    ListTile(\n                      onTap: () async {\n                        final res = await UserHttp.toViewLater(\n                            bvid: widget.videoDetailCtr!.bvid);\n                        SmartDialog.showToast(res['msg']);\n                        Get.back();\n                      },\n                      dense: true,\n                      leading: const Icon(Icons.watch_later_outlined, size: 20),\n                      title: const Text('添加至「稍后再看」', style: titleStyle),\n                    ),\n                    ListTile(\n                      onTap: () => {Get.back(), scheduleExit()},\n                      dense: true,\n                      leading:\n                          const Icon(Icons.hourglass_top_outlined, size: 20),\n                      title: const Text('定时关闭', style: titleStyle),\n                    ),\n                    ListTile(\n                      onTap: () => {Get.back(), showSetVideoQa()},\n                      dense: true,\n                      leading: const Icon(Icons.play_circle_outline, size: 20),\n                      title: const Text('选择画质', style: titleStyle),\n                      subtitle: Text(\n                          '当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}',\n                          style: subTitleStyle),\n                    ),\n                    if (widget.videoDetailCtr!.currentAudioQa != null)\n                      ListTile(\n                        onTap: () => {Get.back(), showSetAudioQa()},\n                        dense: true,\n                        leading: const Icon(Icons.album_outlined, size: 20),\n                        title: const Text('选择音质', style: titleStyle),\n                        subtitle: Text(\n                            '当前音质 ${widget.videoDetailCtr!.currentAudioQa!.description}',\n                            style: subTitleStyle),\n                      ),\n                    if (widget.videoDetailCtr!.currentDecodeFormats != null)\n                      ListTile(\n                        onTap: () => {Get.back(), showSetDecodeFormats()},\n                        dense: true,\n                        leading: const Icon(Icons.av_timer_outlined, size: 20),\n                        title: const Text('解码格式', style: titleStyle),\n                        subtitle: Text(\n                            '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats!.description}',\n                            style: subTitleStyle),\n                      ),\n                    ListTile(\n                      onTap: () => {Get.back(), showSetRepeat()},\n                      dense: true,\n                      leading: const Icon(Icons.repeat, size: 20),\n                      title: const Text('播放顺序', style: titleStyle),\n                      subtitle: Text(widget.controller!.playRepeat.description,\n                          style: subTitleStyle),\n                    ),\n                    ListTile(\n                      onTap: () => {Get.back(), showSetDanmaku()},\n                      dense: true,\n                      leading: const Icon(Icons.subtitles_outlined, size: 20),\n                      title: const Text('弹幕设置', style: titleStyle),\n                    ),\n                  ],\n                ),\n              ))\n            ],\n          ),\n        );\n      },\n      clipBehavior: Clip.hardEdge,\n      isScrollControlled: true,\n    );\n  }\n\n  /// 发送弹幕\n  void showShootDanmakuSheet() {\n    final TextEditingController textController = TextEditingController();\n    bool isSending = false; // 追踪是否正在发送\n    showDialog(\n      context: Get.context!,\n      builder: (BuildContext context) {\n        // TODO: 支持更多类型和颜色的弹幕\n        return AlertDialog(\n          title: const Text('发送弹幕（测试）'),\n          content: StatefulBuilder(\n              builder: (BuildContext context, StateSetter setState) {\n            return TextField(\n              controller: textController,\n            );\n          }),\n          actions: [\n            TextButton(\n              onPressed: () => Get.back(),\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            StatefulBuilder(\n                builder: (BuildContext context, StateSetter setState) {\n              return TextButton(\n                onPressed: isSending\n                    ? null\n                    : () async {\n                        final String msg = textController.text;\n                        if (msg.isEmpty) {\n                          SmartDialog.showToast('弹幕内容不能为空');\n                          return;\n                        } else if (msg.length > 100) {\n                          SmartDialog.showToast('弹幕内容不能超过100个字符');\n                          return;\n                        }\n                        setState(() {\n                          isSending = true; // 开始发送，更新状态\n                        });\n                        //修改按钮文字\n                        final dynamic res = await DanmakaHttp.shootDanmaku(\n                          oid: widget.videoDetailCtr!.cid.value,\n                          msg: textController.text,\n                          bvid: widget.videoDetailCtr!.bvid,\n                          progress:\n                              widget.controller!.position.value.inMilliseconds,\n                          type: 1,\n                        );\n                        setState(() {\n                          isSending = false; // 发送结束，更新状态\n                        });\n                        if (res['status']) {\n                          SmartDialog.showToast('发送成功');\n                          // 发送成功，自动预览该弹幕，避免重新请求\n                          // TODO: 暂停状态下预览弹幕仍会移动与计时，可考虑添加到dmSegList或其他方式实现\n                          widget.controller!.danmakuController!.addItems([\n                            DanmakuItem(\n                              msg,\n                              color: Colors.white,\n                              time: widget\n                                  .controller!.position.value.inMilliseconds,\n                              type: DanmakuItemType.scroll,\n                              isSend: true,\n                            )\n                          ]);\n                          Get.back();\n                        } else {\n                          SmartDialog.showToast('发送失败，错误信息为${res['msg']}');\n                        }\n                      },\n                child: Text(isSending ? '发送中...' : '发送'),\n              );\n            })\n          ],\n        );\n      },\n    );\n  }\n\n  /// 定时关闭\n  void scheduleExit() async {\n    const List<int> scheduleTimeChoices = [\n      -1,\n      15,\n      30,\n      60,\n    ];\n    showModalBottomSheet(\n      context: context,\n      elevation: 0,\n      backgroundColor: Colors.transparent,\n      builder: (BuildContext context) {\n        return StatefulBuilder(\n            builder: (BuildContext context, StateSetter setState) {\n          return Container(\n            width: double.infinity,\n            height: 500,\n            clipBehavior: Clip.hardEdge,\n            decoration: BoxDecoration(\n              color: Theme.of(context).colorScheme.surface,\n              borderRadius: const BorderRadius.all(Radius.circular(12)),\n            ),\n            margin: const EdgeInsets.all(12),\n            padding: const EdgeInsets.only(left: 14, right: 14),\n            child: SingleChildScrollView(\n              child: Padding(\n                padding:\n                    const EdgeInsets.symmetric(vertical: 0, horizontal: 20),\n                child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: <Widget>[\n                      const SizedBox(height: 30),\n                      const Center(child: Text('定时关闭', style: titleStyle)),\n                      const SizedBox(height: 10),\n                      for (final int choice in scheduleTimeChoices) ...<Widget>[\n                        ListTile(\n                          onTap: () {\n                            shutdownTimerService.scheduledExitInMinutes =\n                                choice;\n                            shutdownTimerService.startShutdownTimer();\n                            Get.back();\n                          },\n                          contentPadding: const EdgeInsets.only(),\n                          dense: true,\n                          title: Text(choice == -1 ? \"禁用\" : \"$choice分钟后\"),\n                          trailing: shutdownTimerService\n                                      .scheduledExitInMinutes ==\n                                  choice\n                              ? Icon(\n                                  Icons.done,\n                                  color: Theme.of(context).colorScheme.primary,\n                                )\n                              : const SizedBox(),\n                        )\n                      ],\n                      const SizedBox(height: 6),\n                      const Center(\n                          child: SizedBox(\n                        width: 100,\n                        child: Divider(height: 1),\n                      )),\n                      const SizedBox(height: 10),\n                      ListTile(\n                        onTap: () {\n                          shutdownTimerService.waitForPlayingCompleted =\n                              !shutdownTimerService.waitForPlayingCompleted;\n                          setState(() {});\n                        },\n                        dense: true,\n                        contentPadding: const EdgeInsets.only(),\n                        title: const Text(\"额外等待视频播放完毕\", style: titleStyle),\n                        trailing: Switch(\n                          // thumb color (round icon)\n                          activeColor: Theme.of(context).colorScheme.primary,\n                          activeTrackColor:\n                              Theme.of(context).colorScheme.primaryContainer,\n                          inactiveThumbColor:\n                              Theme.of(context).colorScheme.primaryContainer,\n                          inactiveTrackColor:\n                              Theme.of(context).colorScheme.surface,\n                          splashRadius: 10.0,\n                          // boolean variable value\n                          value: shutdownTimerService.waitForPlayingCompleted,\n                          // changes the state of the switch\n                          onChanged: (value) => setState(() =>\n                              shutdownTimerService.waitForPlayingCompleted =\n                                  value),\n                        ),\n                      ),\n                      const SizedBox(height: 10),\n                      Row(\n                        children: <Widget>[\n                          const Text('倒计时结束:', style: titleStyle),\n                          const Spacer(),\n                          ActionRowLineItem(\n                            onTap: () {\n                              shutdownTimerService.exitApp = false;\n                              setState(() {});\n                              // Get.back();\n                            },\n                            text: \" 暂停视频 \",\n                            selectStatus: !shutdownTimerService.exitApp,\n                          ),\n                          const Spacer(),\n                          // const SizedBox(width: 10),\n                          ActionRowLineItem(\n                            onTap: () {\n                              shutdownTimerService.exitApp = true;\n                              setState(() {});\n                              // Get.back();\n                            },\n                            text: \" 退出APP \",\n                            selectStatus: shutdownTimerService.exitApp,\n                          )\n                        ],\n                      ),\n                    ]),\n              ),\n            ),\n          );\n        });\n      },\n    );\n  }\n\n  /// 选择字幕\n  void showSubtitleDialog() async {\n    int tempThemeValue = widget.controller!.subTitleCode.value;\n    final List subtitles = widget.videoDetailCtr!.subtitles;\n    int len = subtitles.length;\n    if (subtitles.firstWhereOrNull((element) => element.id == tempThemeValue) ==\n        null) {\n      tempThemeValue = -1;\n    }\n    showDialog(\n        context: context,\n        builder: (BuildContext context) {\n          return AlertDialog(\n            title: const Text('选择字幕'),\n            contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 18),\n            content: StatefulBuilder(\n              builder: (context, StateSetter setState) {\n                return len == 0\n                    ? const SizedBox(\n                        height: 60,\n                        child: Center(\n                          child: Text('没有字幕'),\n                        ),\n                      )\n                    : SingleChildScrollView(\n                        child: Column(\n                          mainAxisSize: MainAxisSize.min,\n                          children: [\n                            RadioListTile(\n                              value: -1,\n                              title: const Text('关闭字幕'),\n                              groupValue: tempThemeValue,\n                              onChanged: (value) {\n                                tempThemeValue = value!;\n                                widget.controller?.toggleSubtitle(value);\n                                Get.back();\n                              },\n                            ),\n                            ...widget.videoDetailCtr!.subtitles\n                                .map((e) => RadioListTile(\n                                      value: e.id,\n                                      title: Text(e.title),\n                                      groupValue: tempThemeValue,\n                                      onChanged: (value) {\n                                        tempThemeValue = value!;\n                                        widget.controller\n                                            ?.toggleSubtitle(value);\n                                        Get.back();\n                                      },\n                                    ))\n                                .toList(),\n                          ],\n                        ),\n                      );\n              },\n            ),\n          );\n        });\n  }\n\n  /// 选择倍速\n  void showSetSpeedSheet() {\n    final double currentSpeed = widget.controller!.playbackSpeed;\n    showDialog(\n      context: Get.context!,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('播放速度'),\n          content: StatefulBuilder(\n              builder: (BuildContext context, StateSetter setState) {\n            return Wrap(\n              spacing: 8,\n              runSpacing: 2,\n              children: [\n                for (final double i in speedsList) ...<Widget>[\n                  if (i == currentSpeed) ...<Widget>[\n                    FilledButton(\n                      onPressed: () async {\n                        // setState(() => currentSpeed = i),\n                        await widget.controller!.setPlaybackSpeed(i);\n                        Get.back();\n                      },\n                      child: Text(i.toString()),\n                    ),\n                  ] else ...[\n                    FilledButton.tonal(\n                      onPressed: () async {\n                        // setState(() => currentSpeed = i),\n                        await widget.controller!.setPlaybackSpeed(i);\n                        Get.back();\n                      },\n                      child: Text(i.toString()),\n                    ),\n                  ]\n                ]\n              ],\n            );\n          }),\n          actions: <Widget>[\n            TextButton(\n              onPressed: () => Get.back(),\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                await widget.controller!.setDefaultSpeed();\n                Get.back();\n              },\n              child: const Text('默认速度'),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  /// 选择画质\n  void showSetVideoQa() {\n    final List<FormatItem> videoFormat = videoInfo.supportFormats!;\n    final VideoQuality currentVideoQa = widget.videoDetailCtr!.currentVideoQa;\n\n    /// 总质量分类\n    final int totalQaSam = videoFormat.length;\n\n    /// 可用的质量分类\n    int userfulQaSam = 0;\n    if (videoInfo.dash != null) {\n      // dash格式视频一次请求会返回所有可播放的清晰度video\n      final List<VideoItem> video = videoInfo.dash!.video!;\n      final Set<int> idSet = {};\n      for (final VideoItem item in video) {\n        final int id = item.id!;\n        if (!idSet.contains(id)) {\n          idSet.add(id);\n          userfulQaSam++;\n        }\n      }\n    }\n\n    if (videoInfo.durl != null) {\n      // durl格式视频一次请求返回对应清晰度video\n      userfulQaSam = videoFormat.length - 1;\n    }\n\n    showModalBottomSheet(\n      context: context,\n      elevation: 0,\n      backgroundColor: Colors.transparent,\n      builder: (BuildContext context) {\n        return Container(\n          width: double.infinity,\n          height: 310,\n          clipBehavior: Clip.hardEdge,\n          decoration: BoxDecoration(\n            color: Theme.of(context).colorScheme.surface,\n            borderRadius: const BorderRadius.all(Radius.circular(12)),\n          ),\n          margin: const EdgeInsets.all(12),\n          child: Column(\n            children: <Widget>[\n              SizedBox(\n                height: 45,\n                child: GestureDetector(\n                  onTap: () {\n                    SmartDialog.showToast('标灰画质可能需要bilibili会员');\n                  },\n                  child: Row(\n                    mainAxisAlignment: MainAxisAlignment.center,\n                    children: [\n                      const Text('选择画质', style: titleStyle),\n                      SizedBox(width: buttonSpace),\n                      Icon(\n                        Icons.info_outline,\n                        size: 16,\n                        color: Theme.of(context).colorScheme.outline,\n                      )\n                    ],\n                  ),\n                ),\n              ),\n              Expanded(\n                child: Material(\n                  child: Scrollbar(\n                    child: ListView(\n                      children: [\n                        for (int i = 0; i < totalQaSam; i++) ...[\n                          ListTile(\n                            onTap: () {\n                              if (currentVideoQa.code ==\n                                  videoFormat[i].quality) {\n                                return;\n                              }\n                              final int quality = videoFormat[i].quality!;\n                              widget.videoDetailCtr!.currentVideoQa =\n                                  VideoQualityCode.fromCode(quality)!;\n                              widget.videoDetailCtr!.updatePlayer();\n                              Get.back();\n                            },\n                            dense: true,\n                            // 可能包含会员解锁画质\n                            enabled: i >= totalQaSam - userfulQaSam,\n                            contentPadding:\n                                const EdgeInsets.only(left: 20, right: 20),\n                            title: Text(videoFormat[i].newDesc!),\n                            subtitle: Text(\n                              videoFormat[i].format!,\n                              style: subTitleStyle,\n                            ),\n                            trailing: currentVideoQa.code ==\n                                    videoFormat[i].quality\n                                ? Icon(\n                                    Icons.done,\n                                    color:\n                                        Theme.of(context).colorScheme.primary,\n                                  )\n                                : const SizedBox(),\n                          ),\n                        ]\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  /// 选择音质\n  void showSetAudioQa() {\n    final AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa!;\n    final List<AudioItem> audio = videoInfo.dash!.audio!;\n    showModalBottomSheet(\n      context: context,\n      elevation: 0,\n      backgroundColor: Colors.transparent,\n      builder: (BuildContext context) {\n        return Container(\n          width: double.infinity,\n          height: 250,\n          clipBehavior: Clip.hardEdge,\n          decoration: BoxDecoration(\n            color: Theme.of(context).colorScheme.surface,\n            borderRadius: const BorderRadius.all(Radius.circular(12)),\n          ),\n          margin: const EdgeInsets.all(12),\n          child: Column(\n            children: <Widget>[\n              const SizedBox(\n                  height: 45,\n                  child: Center(child: Text('选择音质', style: titleStyle))),\n              Expanded(\n                child: Material(\n                  child: ListView(\n                    children: <Widget>[\n                      for (final AudioItem i in audio) ...<Widget>[\n                        ListTile(\n                          onTap: () {\n                            if (currentAudioQa.code == i.id) {\n                              return;\n                            }\n                            final int quality = i.id!;\n                            widget.videoDetailCtr!.currentAudioQa =\n                                AudioQualityCode.fromCode(quality)!;\n                            widget.videoDetailCtr!.updatePlayer();\n                            Get.back();\n                          },\n                          dense: true,\n                          contentPadding:\n                              const EdgeInsets.only(left: 20, right: 20),\n                          title: Text(i.quality!),\n                          subtitle: Text(\n                            i.codecs!,\n                            style: subTitleStyle,\n                          ),\n                          trailing: currentAudioQa.code == i.id\n                              ? Icon(\n                                  Icons.done,\n                                  color: Theme.of(context).colorScheme.primary,\n                                )\n                              : const SizedBox(),\n                        ),\n                      ]\n                    ],\n                  ),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  // 选择解码格式\n  void showSetDecodeFormats() {\n    // 当前选中的解码格式\n    final VideoDecodeFormats currentDecodeFormats =\n        widget.videoDetailCtr!.currentDecodeFormats!;\n    final VideoItem firstVideo = widget.videoDetailCtr!.firstVideo;\n    // 当前视频可用的解码格式\n    final List<FormatItem> videoFormat = videoInfo.supportFormats!;\n    final List list = videoFormat\n        .firstWhere((FormatItem e) => e.quality == firstVideo.quality!.code)\n        .codecs!;\n\n    showModalBottomSheet(\n      context: context,\n      elevation: 0,\n      backgroundColor: Colors.transparent,\n      builder: (BuildContext context) {\n        return Container(\n          width: double.infinity,\n          height: 250,\n          clipBehavior: Clip.hardEdge,\n          decoration: BoxDecoration(\n            color: Theme.of(context).colorScheme.surface,\n            borderRadius: const BorderRadius.all(Radius.circular(12)),\n          ),\n          margin: const EdgeInsets.all(12),\n          child: Column(\n            children: [\n              const SizedBox(\n                height: 45,\n                child: Center(\n                  child: Text('选择解码格式', style: titleStyle),\n                ),\n              ),\n              Expanded(\n                child: Material(\n                  child: ListView(\n                    children: [\n                      for (var i in list) ...[\n                        ListTile(\n                          onTap: () {\n                            if (i.startsWith(currentDecodeFormats.code)) return;\n                            widget.videoDetailCtr!.currentDecodeFormats =\n                                VideoDecodeFormatsCode.fromString(i)!;\n                            widget.videoDetailCtr!.updatePlayer();\n                            Get.back();\n                          },\n                          dense: true,\n                          contentPadding:\n                              const EdgeInsets.only(left: 20, right: 20),\n                          title: Text(VideoDecodeFormatsCode.fromString(i)!\n                              .description!),\n                          subtitle: Text(\n                            i!,\n                            style: subTitleStyle,\n                          ),\n                          trailing: i.startsWith(currentDecodeFormats.code)\n                              ? Icon(\n                                  Icons.done,\n                                  color: Theme.of(context).colorScheme.primary,\n                                )\n                              : const SizedBox(),\n                        ),\n                      ]\n                    ],\n                  ),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  /// 弹幕功能\n  void showSetDanmaku() async {\n    // 屏蔽类型\n    final List<Map<String, dynamic>> blockTypesList = [\n      {'value': 5, 'label': '顶部'},\n      {'value': 2, 'label': '滚动'},\n      {'value': 4, 'label': '底部'},\n      {'value': 6, 'label': '彩色'},\n    ];\n    final List blockTypes = widget.controller!.blockTypes;\n    // 显示区域\n    final List<Map<String, dynamic>> showAreas = [\n      {'value': 0.25, 'label': '1/4屏'},\n      {'value': 0.5, 'label': '半屏'},\n      {'value': 0.75, 'label': '3/4屏'},\n      {'value': 1.0, 'label': '满屏'},\n    ];\n    double showArea = widget.controller!.showArea;\n    // 不透明度\n    double opacityVal = widget.controller!.opacityVal;\n    // 字体大小\n    double fontSizeVal = widget.controller!.fontSizeVal;\n    // 弹幕速度\n    double danmakuDurationVal = widget.controller!.danmakuDurationVal;\n    // 弹幕描边\n    double strokeWidth = widget.controller!.strokeWidth;\n\n    final DanmakuController danmakuController =\n        widget.controller!.danmakuController!;\n    await showModalBottomSheet(\n      context: context,\n      elevation: 0,\n      backgroundColor: Colors.transparent,\n      builder: (BuildContext context) {\n        return StatefulBuilder(\n            builder: (BuildContext context, StateSetter setState) {\n          return Container(\n            width: double.infinity,\n            height: 580,\n            clipBehavior: Clip.hardEdge,\n            decoration: BoxDecoration(\n              color: Theme.of(context).colorScheme.surface,\n              borderRadius: const BorderRadius.all(Radius.circular(12)),\n            ),\n            margin: const EdgeInsets.all(12),\n            padding: const EdgeInsets.only(left: 14, right: 14),\n            child: SingleChildScrollView(\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  const SizedBox(\n                    height: 45,\n                    child: Center(child: Text('弹幕设置', style: titleStyle)),\n                  ),\n                  const SizedBox(height: 10),\n                  const Text('按类型屏蔽'),\n                  Padding(\n                    padding: const EdgeInsets.only(top: 12, bottom: 18),\n                    child: Row(\n                      children: <Widget>[\n                        for (final Map<String, dynamic> i\n                            in blockTypesList) ...<Widget>[\n                          ActionRowLineItem(\n                            onTap: () async {\n                              final bool isChoose =\n                                  blockTypes.contains(i['value']);\n                              if (isChoose) {\n                                blockTypes.remove(i['value']);\n                              } else {\n                                blockTypes.add(i['value']);\n                              }\n                              widget.controller!.blockTypes = blockTypes;\n                              setState(() {});\n                              try {\n                                final DanmakuOption currentOption =\n                                    danmakuController.option;\n                                final DanmakuOption updatedOption =\n                                    currentOption.copyWith(\n                                  hideTop: blockTypes.contains(5),\n                                  hideBottom: blockTypes.contains(4),\n                                  hideScroll: blockTypes.contains(2),\n                                  // 添加或修改其他需要修改的选项属性\n                                );\n                                danmakuController.updateOption(updatedOption);\n                              } catch (_) {}\n                            },\n                            text: i['label'],\n                            selectStatus: blockTypes.contains(i['value']),\n                          ),\n                          const SizedBox(width: 10),\n                        ]\n                      ],\n                    ),\n                  ),\n                  const Text('显示区域'),\n                  Padding(\n                    padding: const EdgeInsets.only(top: 12, bottom: 18),\n                    child: Row(\n                      children: [\n                        for (final Map<String, dynamic> i in showAreas) ...[\n                          ActionRowLineItem(\n                            onTap: () {\n                              showArea = i['value'];\n                              widget.controller!.showArea = showArea;\n                              setState(() {});\n                              try {\n                                final DanmakuOption currentOption =\n                                    danmakuController.option;\n                                final DanmakuOption updatedOption =\n                                    currentOption.copyWith(area: i['value']);\n                                danmakuController.updateOption(updatedOption);\n                              } catch (_) {}\n                            },\n                            text: i['label'],\n                            selectStatus: showArea == i['value'],\n                          ),\n                          const SizedBox(width: 10),\n                        ]\n                      ],\n                    ),\n                  ),\n                  Text('不透明度 ${opacityVal * 100}%'),\n                  Padding(\n                    padding: const EdgeInsets.only(\n                      top: 0,\n                      bottom: 6,\n                      left: 10,\n                      right: 10,\n                    ),\n                    child: SliderTheme(\n                      data: SliderThemeData(\n                        trackShape: MSliderTrackShape(),\n                        thumbColor: Theme.of(context).colorScheme.primary,\n                        activeTrackColor: Theme.of(context).colorScheme.primary,\n                        trackHeight: 10,\n                        thumbShape: const RoundSliderThumbShape(\n                            enabledThumbRadius: 6.0),\n                      ),\n                      child: Slider(\n                        min: 0,\n                        max: 1,\n                        value: opacityVal,\n                        divisions: 10,\n                        label: '${opacityVal * 100}%',\n                        onChanged: (double val) {\n                          opacityVal = val;\n                          widget.controller!.opacityVal = opacityVal;\n                          setState(() {});\n                          try {\n                            final DanmakuOption currentOption =\n                                danmakuController.option;\n                            final DanmakuOption updatedOption =\n                                currentOption.copyWith(opacity: val);\n                            danmakuController.updateOption(updatedOption);\n                          } catch (_) {}\n                        },\n                      ),\n                    ),\n                  ),\n                  Text('描边粗细 $strokeWidth'),\n                  Padding(\n                    padding: const EdgeInsets.only(\n                      top: 0,\n                      bottom: 6,\n                      left: 10,\n                      right: 10,\n                    ),\n                    child: SliderTheme(\n                      data: SliderThemeData(\n                        trackShape: MSliderTrackShape(),\n                        thumbColor: Theme.of(context).colorScheme.primary,\n                        activeTrackColor: Theme.of(context).colorScheme.primary,\n                        trackHeight: 10,\n                        thumbShape: const RoundSliderThumbShape(\n                            enabledThumbRadius: 6.0),\n                      ),\n                      child: Slider(\n                        min: 0,\n                        max: 3,\n                        value: strokeWidth,\n                        divisions: 6,\n                        label: '$strokeWidth',\n                        onChanged: (double val) {\n                          strokeWidth = val;\n                          widget.controller!.strokeWidth = val;\n                          setState(() {});\n                          try {\n                            final DanmakuOption currentOption =\n                                danmakuController.option;\n                            final DanmakuOption updatedOption =\n                                currentOption.copyWith(strokeWidth: val);\n                            danmakuController.updateOption(updatedOption);\n                          } catch (_) {}\n                        },\n                      ),\n                    ),\n                  ),\n                  Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'),\n                  Padding(\n                    padding: const EdgeInsets.only(\n                      top: 0,\n                      bottom: 6,\n                      left: 10,\n                      right: 10,\n                    ),\n                    child: SliderTheme(\n                      data: SliderThemeData(\n                        trackShape: MSliderTrackShape(),\n                        thumbColor: Theme.of(context).colorScheme.primary,\n                        activeTrackColor: Theme.of(context).colorScheme.primary,\n                        trackHeight: 10,\n                        thumbShape: const RoundSliderThumbShape(\n                            enabledThumbRadius: 6.0),\n                      ),\n                      child: Slider(\n                        min: 0.5,\n                        max: 2.5,\n                        value: fontSizeVal,\n                        divisions: 20,\n                        label: '${(fontSizeVal * 100).toStringAsFixed(1)}%',\n                        onChanged: (double val) {\n                          fontSizeVal = val;\n                          widget.controller!.fontSizeVal = fontSizeVal;\n                          setState(() {});\n                          try {\n                            final DanmakuOption currentOption =\n                                danmakuController.option;\n                            final DanmakuOption updatedOption =\n                                currentOption.copyWith(\n                              fontSize: (15 * fontSizeVal).toDouble(),\n                            );\n                            danmakuController.updateOption(updatedOption);\n                          } catch (_) {}\n                        },\n                      ),\n                    ),\n                  ),\n                  Text('弹幕时长 ${danmakuDurationVal.toString()} 秒'),\n                  Padding(\n                    padding: const EdgeInsets.only(\n                      top: 0,\n                      bottom: 6,\n                      left: 10,\n                      right: 10,\n                    ),\n                    child: SliderTheme(\n                      data: SliderThemeData(\n                        trackShape: MSliderTrackShape(),\n                        thumbColor: Theme.of(context).colorScheme.primary,\n                        activeTrackColor: Theme.of(context).colorScheme.primary,\n                        trackHeight: 10,\n                        thumbShape: const RoundSliderThumbShape(\n                            enabledThumbRadius: 6.0),\n                      ),\n                      child: Slider(\n                        min: 2,\n                        max: 16,\n                        value: danmakuDurationVal,\n                        divisions: 28,\n                        label: danmakuDurationVal.toString(),\n                        onChanged: (double val) {\n                          danmakuDurationVal = val;\n                          widget.controller!.danmakuDurationVal =\n                              danmakuDurationVal;\n                          setState(() {});\n                          try {\n                            final DanmakuOption updatedOption =\n                                danmakuController.option.copyWith(\n                                    duration:\n                                        val / widget.controller!.playbackSpeed);\n                            danmakuController.updateOption(updatedOption);\n                          } catch (_) {}\n                        },\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          );\n        });\n      },\n    ).then((value) {\n      widget.controller!.cacheDanmakuOption();\n    });\n  }\n\n  /// 播放顺序\n  void showSetRepeat() async {\n    showModalBottomSheet(\n      context: context,\n      elevation: 0,\n      backgroundColor: Colors.transparent,\n      builder: (BuildContext context) {\n        return Container(\n          width: double.infinity,\n          height: 250,\n          clipBehavior: Clip.hardEdge,\n          decoration: BoxDecoration(\n            color: Theme.of(context).colorScheme.surface,\n            borderRadius: const BorderRadius.all(Radius.circular(12)),\n          ),\n          margin: const EdgeInsets.all(12),\n          child: Column(\n            children: [\n              const SizedBox(\n                height: 45,\n                child: Center(\n                  child: Text('选择播放顺序', style: titleStyle),\n                ),\n              ),\n              Expanded(\n                child: Material(\n                  child: ListView(\n                    children: <Widget>[\n                      for (final PlayRepeat i in PlayRepeat.values) ...[\n                        ListTile(\n                          onTap: () {\n                            widget.controller!.setPlayRepeat(i);\n                            Get.back();\n                          },\n                          dense: true,\n                          contentPadding:\n                              const EdgeInsets.only(left: 20, right: 20),\n                          title: Text(i.description),\n                          trailing: widget.controller!.playRepeat == i\n                              ? Icon(\n                                  Icons.done,\n                                  color: Theme.of(context).colorScheme.primary,\n                                )\n                              : const SizedBox(),\n                        )\n                      ],\n                    ],\n                  ),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final _ = widget.controller!;\n    const TextStyle textStyle = TextStyle(\n      color: Colors.white,\n      fontSize: 12,\n    );\n    final bool isLandscape =\n        MediaQuery.of(context).orientation == Orientation.landscape;\n    return AppBar(\n      backgroundColor: Colors.transparent,\n      foregroundColor: Colors.white,\n      elevation: 0,\n      scrolledUnderElevation: 0,\n      primary: false,\n      centerTitle: false,\n      automaticallyImplyLeading: false,\n      titleSpacing: 14,\n      title: Row(\n        children: [\n          ComBtn(\n            icon: const Icon(\n              FontAwesomeIcons.arrowLeft,\n              size: 15,\n              color: Colors.white,\n            ),\n            fuc: () => <Set<void>>{\n              if (widget.controller!.isFullScreen.value)\n                <void>{widget.controller!.triggerFullScreen(status: false)}\n              else\n                <void>{\n                  if (MediaQuery.of(context).orientation ==\n                      Orientation.landscape)\n                    {\n                      SystemChrome.setPreferredOrientations([\n                        DeviceOrientation.portraitUp,\n                      ])\n                    },\n                  Get.back()\n                }\n            },\n          ),\n          SizedBox(width: buttonSpace),\n          if (isFullScreen.value &&\n              isLandscape &&\n              widget.videoType == SearchType.video) ...[\n            Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                ConstrainedBox(\n                  constraints: const BoxConstraints(maxWidth: 200),\n                  child: Obx(\n                    () => Text(\n                      videoIntroController.videoDetail.value.title ?? '',\n                      style: const TextStyle(\n                        color: Colors.white,\n                        fontSize: 16,\n                      ),\n                    ),\n                  ),\n                ),\n                if (videoIntroController.isShowOnlineTotal)\n                  Text(\n                    '${videoIntroController.total.value}人正在看',\n                    style: const TextStyle(\n                      color: Colors.white,\n                      fontSize: 12,\n                    ),\n                  )\n              ],\n            )\n          ] else ...[\n            ComBtn(\n              icon: const Icon(\n                FontAwesomeIcons.house,\n                size: 15,\n                color: Colors.white,\n              ),\n              fuc: () async {\n                // 销毁播放器实例\n                await widget.controller!.dispose(type: 'all');\n                if (mounted) {\n                  Navigator.popUntil(\n                      context, (Route<dynamic> route) => route.isFirst);\n                }\n              },\n            ),\n          ],\n          const Spacer(),\n          // ComBtn(\n          //   icon: const Icon(\n          //     FontAwesomeIcons.cropSimple,\n          //     size: 15,\n          //     color: Colors.white,\n          //   ),\n          //   fuc: () => _.screenshot(),\n          // ),\n          ComBtn(\n            icon: const Icon(\n              Icons.cast,\n              size: 19,\n              color: Colors.white,\n            ),\n            fuc: () async {\n              showDialog<void>(\n                context: context,\n                builder: (BuildContext context) {\n                  return LiveDlnaPage(\n                      datasource: widget.videoDetailCtr!.videoUrl);\n                },\n              );\n            },\n          ),\n          if (isFullScreen.value) ...[\n            SizedBox(\n              width: 56,\n              height: 34,\n              child: TextButton(\n                style: ButtonStyle(\n                  padding: MaterialStateProperty.all(EdgeInsets.zero),\n                ),\n                onPressed: () => showShootDanmakuSheet(),\n                child: const Text(\n                  '发弹幕',\n                  style: textStyle,\n                ),\n              ),\n            ),\n            SizedBox(\n              width: 34,\n              height: 34,\n              child: Obx(\n                () => IconButton(\n                  style: ButtonStyle(\n                    padding: MaterialStateProperty.all(EdgeInsets.zero),\n                  ),\n                  onPressed: () {\n                    _.isOpenDanmu.value = !_.isOpenDanmu.value;\n                  },\n                  icon: Icon(\n                    _.isOpenDanmu.value\n                        ? Icons.subtitles_outlined\n                        : Icons.subtitles_off_outlined,\n                    size: 19,\n                    color: Colors.white,\n                  ),\n                ),\n              ),\n            ),\n          ],\n          SizedBox(width: buttonSpace),\n          if (Platform.isAndroid) ...<Widget>[\n            SizedBox(\n              width: 34,\n              height: 34,\n              child: IconButton(\n                style: ButtonStyle(\n                  padding: MaterialStateProperty.all(EdgeInsets.zero),\n                ),\n                onPressed: () async {\n                  bool canUsePiP = false;\n                  widget.controller!.hiddenControls(false);\n                  try {\n                    canUsePiP = await widget.floating!.isPipAvailable;\n                  } on PlatformException catch (_) {\n                    canUsePiP = false;\n                  }\n                  if (canUsePiP) {\n                    final Rational aspectRatio = Rational(\n                      widget.videoDetailCtr!.data.dash!.video!.first.width!,\n                      widget.videoDetailCtr!.data.dash!.video!.first.height!,\n                    );\n                    await widget.floating!.enable(aspectRatio: aspectRatio);\n                  } else {}\n                },\n                icon: const Icon(\n                  Icons.picture_in_picture_outlined,\n                  size: 19,\n                  color: Colors.white,\n                ),\n              ),\n            ),\n            SizedBox(width: buttonSpace),\n          ],\n\n          /// 字幕\n          if (widget.showSubtitleBtn ?? true)\n            ComBtn(\n              icon: const Icon(\n                Icons.closed_caption_off,\n                size: 22,\n                color: Colors.white,\n              ),\n              fuc: () => showSubtitleDialog(),\n            ),\n          SizedBox(width: buttonSpace),\n          Obx(\n            () => SizedBox(\n              width: 45,\n              height: 34,\n              child: TextButton(\n                style: ButtonStyle(\n                  padding: MaterialStateProperty.all(EdgeInsets.zero),\n                ),\n                onPressed: () => showSetSpeedSheet(),\n                child: Text(\n                  '${_.playbackSpeed}X',\n                  style: textStyle,\n                ),\n              ),\n            ),\n          ),\n          SizedBox(width: buttonSpace),\n          ComBtn(\n            icon: const Icon(\n              Icons.more_vert_outlined,\n              size: 18,\n              color: Colors.white,\n            ),\n            fuc: () => showSettingSheet(),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass MSliderTrackShape extends RoundedRectSliderTrackShape {\n  @override\n  Rect getPreferredRect({\n    required RenderBox parentBox,\n    Offset offset = Offset.zero,\n    SliderThemeData? sliderTheme,\n    bool isEnabled = false,\n    bool isDiscrete = false,\n  }) {\n    const double trackHeight = 3;\n    final double trackLeft = offset.dx;\n    final double trackTop =\n        offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;\n    final double trackWidth = parentBox.size.width;\n    return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/widgets/right_drawer.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass RightDrawer extends StatefulWidget {\n  const RightDrawer({super.key});\n\n  @override\n  State<RightDrawer> createState() => _RightDrawerState();\n}\n\nclass _RightDrawerState extends State<RightDrawer> {\n  @override\n  Widget build(BuildContext context) {\n    return Drawer(\n        shadowColor: Colors.transparent,\n        elevation: 0,\n        backgroundColor:\n            Theme.of(context).colorScheme.surface.withOpacity(0.8));\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/detail/widgets/watch_later_list.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/badge.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/common/widgets/stat/danmu.dart';\nimport 'package:pilipala/common/widgets/stat/view.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/models/video/later.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass MediaListPanel extends StatefulWidget {\n  const MediaListPanel({\n    this.sheetHeight,\n    required this.mediaList,\n    this.changeMediaList,\n    this.panelTitle,\n    this.bvid,\n    this.mediaId,\n    this.hasMore = false,\n    super.key,\n  });\n\n  final double? sheetHeight;\n  final List<MediaVideoItemModel> mediaList;\n  final Function? changeMediaList;\n  final String? panelTitle;\n  final String? bvid;\n  final int? mediaId;\n  final bool hasMore;\n\n  @override\n  State<MediaListPanel> createState() => _MediaListPanelState();\n}\n\nclass _MediaListPanelState extends State<MediaListPanel> {\n  RxList<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[].obs;\n  final ScrollController _scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    mediaList.value = widget.mediaList;\n    _scrollController.addListener(() {\n      if (_scrollController.position.pixels >=\n          _scrollController.position.maxScrollExtent - 200) {\n        if (widget.hasMore) {\n          EasyThrottle.throttle(\n              'queryFollowDynamic', const Duration(seconds: 1), () {\n            loadMore();\n          });\n        }\n      }\n    });\n  }\n\n  void loadMore() async {\n    var res = await UserHttp.getMediaList(\n      type: 3,\n      bizId: widget.mediaId!,\n      ps: 20,\n      oid: mediaList.last.id,\n    );\n    if (res['status']) {\n      if (res['data'].isNotEmpty) {\n        mediaList.addAll(res['data']);\n      }\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: widget.sheetHeight,\n      color: Theme.of(context).colorScheme.surface,\n      child: Column(\n        children: [\n          AppBar(\n            toolbarHeight: 45,\n            automaticallyImplyLeading: false,\n            centerTitle: false,\n            title: Text(\n              widget.panelTitle ?? '稍后再看',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n            actions: [\n              IconButton(\n                icon: const Icon(Icons.close, size: 20),\n                onPressed: () {\n                  Navigator.pop(context);\n                },\n              ),\n              const SizedBox(width: 14),\n            ],\n          ),\n          Expanded(\n            child: Material(\n              color: Theme.of(context).colorScheme.surface,\n              child: Obx(\n                () => ListView.builder(\n                  controller: _scrollController,\n                  itemCount: mediaList.length,\n                  itemBuilder: ((context, index) {\n                    var item = mediaList[index];\n                    return InkWell(\n                      onTap: () async {\n                        String bvid = item.bvid!;\n                        int? aid = item.id;\n                        String cover = item.cover ?? '';\n                        final int cid =\n                            await SearchHttp.ab2c(aid: aid, bvid: bvid);\n                        widget.changeMediaList?.call(bvid, cid, aid, cover);\n                      },\n                      child: Padding(\n                        padding: const EdgeInsets.symmetric(\n                            horizontal: 10, vertical: 8),\n                        child: LayoutBuilder(\n                          builder: (BuildContext context,\n                              BoxConstraints boxConstraints) {\n                            const double width = 120;\n                            return Container(\n                              constraints: const BoxConstraints(minHeight: 88),\n                              height: width / StyleString.aspectRatio,\n                              child: Row(\n                                mainAxisAlignment: MainAxisAlignment.start,\n                                crossAxisAlignment: CrossAxisAlignment.start,\n                                children: <Widget>[\n                                  AspectRatio(\n                                    aspectRatio: StyleString.aspectRatio,\n                                    child: LayoutBuilder(\n                                      builder: (BuildContext context,\n                                          BoxConstraints boxConstraints) {\n                                        final double maxWidth =\n                                            boxConstraints.maxWidth;\n                                        final double maxHeight =\n                                            boxConstraints.maxHeight;\n                                        return Stack(\n                                          children: [\n                                            NetworkImgLayer(\n                                              src: item.cover ?? '',\n                                              width: maxWidth,\n                                              height: maxHeight,\n                                            ),\n                                            PBadge(\n                                              text: Utils.timeFormat(\n                                                  item.duration!),\n                                              right: 6.0,\n                                              bottom: 6.0,\n                                              type: 'gray',\n                                            ),\n                                          ],\n                                        );\n                                      },\n                                    ),\n                                  ),\n                                  Expanded(\n                                    child: Padding(\n                                      padding: const EdgeInsets.fromLTRB(\n                                          10, 0, 6, 0),\n                                      child: Column(\n                                        crossAxisAlignment:\n                                            CrossAxisAlignment.start,\n                                        children: [\n                                          Text(\n                                            item.title as String,\n                                            textAlign: TextAlign.start,\n                                            maxLines: 2,\n                                            overflow: TextOverflow.ellipsis,\n                                            style: TextStyle(\n                                              fontWeight: FontWeight.w500,\n                                              color: item.bvid == widget.bvid\n                                                  ? Theme.of(context)\n                                                      .colorScheme\n                                                      .primary\n                                                  : null,\n                                            ),\n                                          ),\n                                          const Spacer(),\n                                          Text(\n                                            item.upper?.name as String,\n                                            style: TextStyle(\n                                              fontSize: Theme.of(context)\n                                                  .textTheme\n                                                  .labelMedium!\n                                                  .fontSize,\n                                              color: Theme.of(context)\n                                                  .colorScheme\n                                                  .outline,\n                                            ),\n                                          ),\n                                          const SizedBox(height: 2),\n                                          Row(\n                                            children: [\n                                              StatView(\n                                                  view: item.cntInfo!['play']\n                                                      as int),\n                                              const SizedBox(width: 8),\n                                              StatDanMu(\n                                                  danmu:\n                                                      item.cntInfo!['danmaku']\n                                                          as int),\n                                            ],\n                                          ),\n                                        ],\n                                      ),\n                                    ),\n                                  )\n                                ],\n                              ),\n                            );\n                          },\n                        ),\n                      ),\n                    );\n                  }),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/webview/controller.dart",
    "content": "import 'package:get/get.dart';\nimport 'package:pilipala/http/init.dart';\nimport 'package:pilipala/utils/event_bus.dart';\nimport 'package:pilipala/utils/id_utils.dart';\nimport 'package:pilipala/utils/login.dart';\nimport 'package:webview_flutter/webview_flutter.dart';\n\nclass WebviewController extends GetxController {\n  String url = '';\n  RxString type = ''.obs;\n  String pageTitle = '';\n  final WebViewController controller = WebViewController();\n  RxInt loadProgress = 0.obs;\n  RxBool loadShow = true.obs;\n  EventBus eventBus = EventBus();\n\n  @override\n  void onInit() {\n    super.onInit();\n    url = Get.parameters['url']!;\n    type.value = Get.parameters['type']!;\n    pageTitle = Get.parameters['pageTitle']!;\n\n    if (type.value == 'login') {\n      controller.clearCache();\n      controller.clearLocalStorage();\n      WebViewCookieManager().clearCookies();\n    }\n    webviewInit();\n  }\n\n  webviewInit() {\n    controller\n      ..setUserAgent(Request().headerUa())\n      ..setJavaScriptMode(JavaScriptMode.unrestricted)\n      ..setNavigationDelegate(\n        NavigationDelegate(\n          // 页面加载\n          onProgress: (int progress) {\n            // Update loading bar.\n            loadProgress.value = progress;\n          },\n          onPageStarted: (String url) {\n            final List pathSegments = Uri.parse(url).pathSegments;\n            if (pathSegments.isNotEmpty &&\n                url != 'https://passport.bilibili.com/h5-app/passport/login') {\n              final String str = pathSegments[0];\n              final Map matchRes = IdUtils.matchAvorBv(input: str);\n              final List matchKeys = matchRes.keys.toList();\n              if (matchKeys.isNotEmpty) {\n                if (matchKeys.first == 'BV') {\n                  Get.offAndToNamed(\n                    '/searchResult',\n                    parameters: {'keyword': matchRes['BV']},\n                  );\n                }\n              }\n            }\n          },\n          // 加载完成\n          onUrlChange: (UrlChange urlChange) async {\n            loadShow.value = false;\n            String url = urlChange.url ?? '';\n            if (type.value == 'login' &&\n                (url.startsWith(\n                        'https://passport.bilibili.com/web/sso/exchange_cookie') ||\n                    url.startsWith('https://m.bilibili.com/'))) {\n              LoginUtils.confirmLogin(url, controller);\n            }\n          },\n          onWebResourceError: (WebResourceError error) {},\n          onNavigationRequest: (NavigationRequest request) {\n            if (request.url.startsWith('bilibili://')) {\n              if (request.url.startsWith('bilibili://video/')) {\n                String str = Uri.parse(request.url).pathSegments[0];\n                Get.offAndToNamed(\n                  '/searchResult',\n                  parameters: {'keyword': str},\n                );\n              }\n              return NavigationDecision.prevent;\n            }\n            return NavigationDecision.navigate;\n          },\n        ),\n      )\n      ..loadRequest(Uri.parse(url.startsWith('http') ? url : 'https://$url'));\n  }\n}\n"
  },
  {
    "path": "lib/pages/webview/index.dart",
    "content": "library webview;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/webview/view.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/utils/login.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport 'controller.dart';\nimport 'package:webview_flutter/webview_flutter.dart';\n\nclass WebviewPage extends StatefulWidget {\n  const WebviewPage({super.key});\n\n  @override\n  State<WebviewPage> createState() => _WebviewPageState();\n}\n\nclass _WebviewPageState extends State<WebviewPage> {\n  final WebviewController _webviewController = Get.put(WebviewController());\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n          centerTitle: false,\n          titleSpacing: 0,\n          title: Text(\n            _webviewController.pageTitle,\n            style: Theme.of(context).textTheme.titleMedium,\n          ),\n          actions: [\n            const SizedBox(width: 4),\n            IconButton(\n              onPressed: () {\n                _webviewController.controller.reload();\n              },\n              icon: Icon(Icons.refresh_outlined,\n                  color: Theme.of(context).colorScheme.primary),\n            ),\n            IconButton(\n              onPressed: () {\n                launchUrl(Uri.parse(_webviewController.url));\n              },\n              icon: Icon(Icons.open_in_browser_outlined,\n                  color: Theme.of(context).colorScheme.primary),\n            ),\n            Obx(\n              () => _webviewController.type.value == 'login'\n                  ? TextButton(\n                      onPressed: () =>\n                          LoginUtils.confirmLogin(null, _webviewController),\n                      child: const Text('刷新登录状态'),\n                    )\n                  : const SizedBox(),\n            ),\n            const SizedBox(width: 12)\n          ],\n        ),\n        body: Column(\n          children: [\n            Obx(\n              () => AnimatedContainer(\n                curve: Curves.easeInOut,\n                duration: const Duration(milliseconds: 350),\n                height: _webviewController.loadShow.value ? 4 : 0,\n                child: LinearProgressIndicator(\n                  key: ValueKey(_webviewController.loadProgress),\n                  value: _webviewController.loadProgress / 100,\n                ),\n              ),\n            ),\n            if (_webviewController.type.value == 'login')\n              Container(\n                width: double.infinity,\n                color: Theme.of(context).colorScheme.onInverseSurface,\n                padding: const EdgeInsets.only(\n                    left: 12, right: 12, top: 6, bottom: 6),\n                child: const Text('登录成功未自动跳转?  请点击右上角「刷新登录状态」'),\n              ),\n            Expanded(\n              child: WebViewWidget(controller: _webviewController.controller),\n            ),\n          ],\n        ));\n  }\n}\n"
  },
  {
    "path": "lib/pages/whisper/controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/msg.dart';\nimport 'package:pilipala/models/msg/account.dart';\nimport 'package:pilipala/models/msg/session.dart';\n\nclass WhisperController extends GetxController {\n  RxList<SessionList> sessionList = <SessionList>[].obs;\n  RxList<AccountListModel> accountList = <AccountListModel>[].obs;\n  bool isLoading = false;\n  RxList noticesList = [\n    {\n      'icon': Icons.message_outlined,\n      'title': '回复我的',\n      'path': '/messageReply',\n      'count': 0,\n    },\n    {\n      'icon': Icons.alternate_email,\n      'title': '@我的',\n      'path': '/messageAt',\n      'count': 0,\n    },\n    {\n      'icon': Icons.thumb_up_outlined,\n      'title': '收到的赞',\n      'path': '/messageLike',\n      'count': 0,\n    },\n    {\n      'icon': Icons.notifications_none_outlined,\n      'title': '系统通知',\n      'path': '/messageSystem',\n      'count': 0,\n    }\n  ].obs;\n\n  @override\n  void onInit() {\n    unread();\n    super.onInit();\n  }\n\n  Future querySessionList(String? type) async {\n    if (isLoading) return;\n    var res = await MsgHttp.sessionList(\n        endTs: type == 'onLoad' ? sessionList.last.sessionTs : null);\n    if (res['data'].sessionList != null && res['data'].sessionList.isNotEmpty) {\n      await queryAccountList(res['data'].sessionList);\n      // 将 accountList 转换为 Map 结构\n      Map<int, dynamic> accountMap = {};\n      for (var j in accountList) {\n        accountMap[j.mid!] = j;\n      }\n\n      // 遍历 sessionList，通过 mid 查找并赋值 accountInfo\n      for (var i in res['data'].sessionList) {\n        var accountInfo = accountMap[i.talkerId];\n        if (accountInfo != null) {\n          i.accountInfo = accountInfo;\n        }\n        if (i.talkerId == 844424930131966) {\n          i.accountInfo = AccountListModel(\n            name: 'UP主小助手',\n            face:\n                'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png',\n          );\n        }\n      }\n    }\n    if (res['status'] && res['data'].sessionList != null) {\n      if (type == 'onLoad') {\n        sessionList.addAll(res['data'].sessionList);\n      } else {\n        sessionList.value = res['data'].sessionList;\n      }\n    }\n    isLoading = false;\n    return res;\n  }\n\n  Future queryAccountList(sessionList) async {\n    List midsList = sessionList.map((e) => e.talkerId!).toList();\n    var res = await MsgHttp.accountList(midsList.join(','));\n    if (res['status']) {\n      accountList.value = res['data'];\n    }\n    return res;\n  }\n\n  Future onLoad() async {\n    querySessionList('onLoad');\n  }\n\n  Future onRefresh() async {\n    querySessionList('onRefresh');\n  }\n\n  void refreshLastMsg(int talkerId, String content) {\n    final SessionList currentItem =\n        sessionList.where((p0) => p0.talkerId == talkerId).first;\n    currentItem.lastMsg!.content['content'] = content;\n    sessionList.removeWhere((p0) => p0.talkerId == talkerId);\n    sessionList.insert(0, currentItem);\n    sessionList.refresh();\n  }\n\n  // 移除会话\n  void removeSessionMsg(int talkerId) {\n    sessionList.removeWhere((p0) => p0.talkerId == talkerId);\n    sessionList.refresh();\n  }\n\n  // 消息未读数\n  void unread() async {\n    var res = await MsgHttp.unread();\n    if (res['status']) {\n      noticesList[0]['count'] = res['data']['reply'];\n      noticesList[1]['count'] = res['data']['at'];\n      noticesList[2]['count'] = res['data']['like'];\n      noticesList[3]['count'] = res['data']['sys_msg'];\n      noticesList.refresh();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/whisper/index.dart",
    "content": "library whisper;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/whisper/view.dart",
    "content": "import 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/skeleton/skeleton.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nimport 'controller.dart';\n\nclass WhisperPage extends StatefulWidget {\n  const WhisperPage({super.key});\n\n  @override\n  State<WhisperPage> createState() => _WhisperPageState();\n}\n\nclass _WhisperPageState extends State<WhisperPage> {\n  late final WhisperController _whisperController =\n      Get.put(WhisperController());\n  late Future _futureBuilderFuture;\n  final ScrollController _scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    _futureBuilderFuture = _whisperController.querySessionList('init');\n    _scrollController.addListener(_scrollListener);\n  }\n\n  Future _scrollListener() async {\n    if (_scrollController.position.pixels >=\n        _scrollController.position.maxScrollExtent - 200) {\n      EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800),\n          () async {\n        await _whisperController.onLoad();\n        _whisperController.isLoading = true;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('消息'),\n      ),\n      body: RefreshIndicator(\n        onRefresh: () async {\n          _whisperController.unread();\n          await _whisperController.onRefresh();\n        },\n        child: SingleChildScrollView(\n          controller: _scrollController,\n          child: Column(\n            children: [\n              LayoutBuilder(\n                builder: (BuildContext context, BoxConstraints constraints) {\n                  // 在这里根据父级容器的约束条件构建小部件树\n                  return Padding(\n                    padding: const EdgeInsets.only(left: 20, right: 20),\n                    child: SizedBox(\n                      height: constraints.maxWidth / 4,\n                      child: Obx(\n                        () => GridView.count(\n                          primary: false,\n                          crossAxisCount: 4,\n                          padding: const EdgeInsets.all(0),\n                          children: [\n                            ..._whisperController.noticesList.map((element) {\n                              return InkWell(\n                                onTap: () {\n                                  if (['/messageAt']\n                                      .contains(element['path'])) {\n                                    SmartDialog.showToast('功能开发中');\n                                    return;\n                                  }\n                                  Get.toNamed(element['path']);\n\n                                  if (element['count'] > 0) {\n                                    element['count'] = 0;\n                                  }\n                                  _whisperController.noticesList.refresh();\n                                },\n                                onLongPress: () {},\n                                borderRadius: StyleString.mdRadius,\n                                child: Column(\n                                  mainAxisAlignment: MainAxisAlignment.center,\n                                  children: [\n                                    Badge(\n                                      isLabelVisible: element['count'] > 0,\n                                      label: Text(element['count'] > 99\n                                          ? '99+'\n                                          : element['count'].toString()),\n                                      child: Padding(\n                                        padding: const EdgeInsets.all(10),\n                                        child: Icon(\n                                          element['icon'],\n                                          size: 21,\n                                          color: Theme.of(context)\n                                              .colorScheme\n                                              .primary,\n                                        ),\n                                      ),\n                                    ),\n                                    const SizedBox(height: 4),\n                                    Text(element['title'])\n                                  ],\n                                ),\n                              );\n                            }).toList(),\n                          ],\n                        ),\n                      ),\n                    ),\n                  );\n                },\n              ),\n              FutureBuilder(\n                future: _futureBuilderFuture,\n                builder: (context, snapshot) {\n                  if (snapshot.connectionState == ConnectionState.done) {\n                    Map? data = snapshot.data;\n                    if (data != null && data['status']) {\n                      RxList sessionList = _whisperController.sessionList;\n                      return Obx(\n                        () => sessionList.isEmpty\n                            ? const SizedBox()\n                            : ListView.separated(\n                                itemCount: sessionList.length,\n                                shrinkWrap: true,\n                                physics: const NeverScrollableScrollPhysics(),\n                                itemBuilder: (_, int i) {\n                                  return SessionItem(\n                                    sessionItem: sessionList[i],\n                                    changeFucCall: () => sessionList.refresh(),\n                                  );\n                                },\n                                separatorBuilder:\n                                    (BuildContext context, int index) {\n                                  return Divider(\n                                    indent: 72,\n                                    endIndent: 20,\n                                    height: 6,\n                                    color: Colors.grey.withOpacity(0.1),\n                                  );\n                                },\n                              ),\n                      );\n                    } else {\n                      // 请求错误\n                      return Center(\n                        child: Text(data?['msg'] ?? '请求异常'),\n                      );\n                    }\n                  } else {\n                    // 骨架屏\n                    return ListView.builder(\n                      itemCount: 15,\n                      shrinkWrap: true,\n                      physics: const NeverScrollableScrollPhysics(),\n                      itemBuilder: (context, int i) {\n                        return Skeleton(\n                          child: ListTile(\n                            leading: Container(\n                              width: 45,\n                              height: 45,\n                              decoration: BoxDecoration(\n                                color: Theme.of(context)\n                                    .colorScheme\n                                    .onInverseSurface,\n                                borderRadius: BorderRadius.circular(25),\n                              ),\n                            ),\n                            title: Container(\n                              width: 100,\n                              height: 14,\n                              color: Theme.of(context)\n                                  .colorScheme\n                                  .onInverseSurface,\n                            ),\n                            subtitle: Container(\n                              width: 80,\n                              height: 14,\n                              color: Theme.of(context)\n                                  .colorScheme\n                                  .onInverseSurface,\n                            ),\n                          ),\n                        );\n                      },\n                    );\n                  }\n                },\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass SessionItem extends StatelessWidget {\n  final dynamic sessionItem;\n  final Function changeFucCall;\n\n  const SessionItem({\n    super.key,\n    required this.sessionItem,\n    required this.changeFucCall,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo?.mid ?? 0);\n    final content = sessionItem.lastMsg.content;\n    final msgStatus = sessionItem.lastMsg.msgStatus;\n    final int msgType = sessionItem.lastMsg.msgType;\n\n    return ListTile(\n      onTap: () {\n        sessionItem.unreadCount = 0;\n        changeFucCall.call();\n        Get.toNamed(\n          '/whisperDetail',\n          parameters: {\n            'talkerId': sessionItem.talkerId.toString(),\n            'name': sessionItem.accountInfo.name,\n            'face': sessionItem.accountInfo.face,\n            'mid': (sessionItem.accountInfo?.mid ?? 0).toString(),\n            'heroTag': heroTag,\n          },\n        );\n      },\n      leading: Badge(\n        isLabelVisible: sessionItem.unreadCount > 0,\n        label: Text(sessionItem.unreadCount.toString()),\n        alignment: Alignment.topRight,\n        child: Hero(\n          tag: heroTag,\n          child: NetworkImgLayer(\n            width: 45,\n            height: 45,\n            type: 'avatar',\n            src: sessionItem.accountInfo.face,\n          ),\n        ),\n      ),\n      title: Text(sessionItem.accountInfo.name),\n      subtitle: Text(\n          msgStatus == 1\n              ? '你撤回了一条消息'\n              : msgType == 2\n                  ? '[图片]'\n                  : content != null && content != ''\n                      ? (content['text'] ??\n                          content['content'] ??\n                          content['title'] ??\n                          content['reply_content'] ??\n                          '不支持的消息类型')\n                      : '不支持的消息类型',\n          maxLines: 1,\n          overflow: TextOverflow.ellipsis,\n          style: Theme.of(context)\n              .textTheme\n              .labelMedium!\n              .copyWith(color: Theme.of(context).colorScheme.outline)),\n      trailing: Text(\n        Utils.dateFormat(sessionItem.lastMsg.timestamp),\n        style: TextStyle(\n          fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,\n          color: Theme.of(context).colorScheme.outline,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/whisper_detail/controller.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/msg.dart';\nimport 'package:pilipala/models/msg/session.dart';\nimport 'package:pilipala/pages/whisper/index.dart';\nimport '../../utils/feed_back.dart';\nimport '../../utils/storage.dart';\n\nclass WhisperDetailController extends GetxController {\n  int? talkerId;\n  late String name;\n  late String face;\n  late String mid;\n  late String heroTag;\n  RxList<MessageItem> messageList = <MessageItem>[].obs;\n  //表情转换图片规则\n  RxList<dynamic> eInfos = [].obs;\n  final TextEditingController replyContentController = TextEditingController();\n  Box userInfoCache = GStrorage.userInfo;\n  List emoteList = [];\n  List<String> picList = [];\n\n  @override\n  void onInit() {\n    super.onInit();\n    if (Get.parameters.containsKey('talkerId')) {\n      talkerId = int.parse(Get.parameters['talkerId']!);\n    } else {\n      talkerId = int.parse(Get.parameters['mid']!);\n    }\n    name = Get.parameters['name']!;\n    face = Get.parameters['face']!;\n    mid = Get.parameters['mid']!;\n    heroTag = Get.parameters['heroTag']!;\n  }\n\n  Future querySessionMsg() async {\n    var res = await MsgHttp.sessionMsg(talkerId: talkerId);\n    if (res['status']) {\n      messageList.value = res['data'].messages;\n      // 找出图片\n      try {\n        for (var item in messageList) {\n          if (item.msgType == 2) {\n            picList.add(item.content['url']);\n          }\n        }\n        picList = picList.reversed.toList();\n      } catch (e) {\n        print('e: $e');\n      }\n\n      if (messageList.isNotEmpty) {\n        ackSessionMsg();\n        if (res['data'].eInfos != null) {\n          eInfos.value = res['data'].eInfos;\n        }\n      }\n    } else {\n      SmartDialog.showToast(res['msg']);\n    }\n    return res;\n  }\n\n  // 消息标记已读\n  Future ackSessionMsg() async {\n    if (messageList.isEmpty) {\n      return;\n    }\n    await MsgHttp.ackSessionMsg(\n      talkerId: talkerId,\n      ackSeqno: messageList.last.msgSeqno,\n    );\n  }\n\n  Future sendMsg() async {\n    feedBack();\n    String message = replyContentController.text;\n    final userInfo = userInfoCache.get('userInfoCache');\n    if (userInfo == null) {\n      SmartDialog.showToast('请先登录');\n      return;\n    }\n    if (message == '') {\n      SmartDialog.showToast('请输入内容');\n      return;\n    }\n    var result = await MsgHttp.sendMsg(\n      senderUid: userInfo.mid,\n      receiverId: int.parse(mid),\n      content: {'content': message},\n      msgType: 1,\n    );\n    if (result['status']) {\n      String content = jsonDecode(result['data']['msg_content'])['content'];\n      messageList.insert(\n        0,\n        MessageItem(\n          msgSeqno: result['data']['msg_key'],\n          senderUid: userInfo.mid,\n          receiverId: int.parse(mid),\n          content: {'content': content},\n          msgType: 1,\n          timestamp: DateTime.now().millisecondsSinceEpoch,\n        ),\n      );\n      eInfos.addAll(emoteList);\n      replyContentController.clear();\n      try {\n        late final WhisperController whisperController =\n            Get.find<WhisperController>();\n        whisperController.refreshLastMsg(talkerId!, message);\n      } catch (_) {}\n    } else {\n      SmartDialog.showToast(result['msg']);\n    }\n  }\n\n  void removeSession(context) {\n    showDialog(\n      context: context,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          clipBehavior: Clip.hardEdge,\n          title: const Text('提示'),\n          content: const Text('确认清空会话内容并移除会话？'),\n          actions: [\n            TextButton(\n              onPressed: Get.back,\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                var res = await MsgHttp.removeSession(talkerId: talkerId);\n                if (res['status']) {\n                  SmartDialog.showToast('操作成功');\n                  try {\n                    late final WhisperController whisperController =\n                        Get.find<WhisperController>();\n                    whisperController.removeSessionMsg(talkerId!);\n                    Get.back();\n                  } catch (_) {}\n                }\n              },\n              child: const Text('确认'),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/whisper_detail/index.dart",
    "content": "library whisper_detail;\n\nexport './controller.dart';\nexport './view.dart';\n"
  },
  {
    "path": "lib/pages/whisper_detail/view.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/models/video/reply/emote.dart';\nimport 'package:pilipala/pages/emote/index.dart';\nimport 'package:pilipala/pages/video/detail/reply_new/toolbar_icon_button.dart';\nimport 'package:pilipala/pages/whisper_detail/controller.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport '../../utils/storage.dart';\nimport 'widget/chat_item.dart';\n\nclass WhisperDetailPage extends StatefulWidget {\n  const WhisperDetailPage({super.key});\n\n  @override\n  State<WhisperDetailPage> createState() => _WhisperDetailPageState();\n}\n\nclass _WhisperDetailPageState extends State<WhisperDetailPage>\n    with WidgetsBindingObserver {\n  final WhisperDetailController _whisperDetailController =\n      Get.put(WhisperDetailController());\n  late Future _futureBuilderFuture;\n  late TextEditingController _replyContentController;\n  final FocusNode replyContentFocusNode = FocusNode();\n  final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间\n  late double emoteHeight = 230.0;\n  double keyboardHeight = 0.0; // 键盘高度\n  RxString toolbarType = ''.obs;\n  Box userInfoCache = GStrorage.userInfo;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    _futureBuilderFuture = _whisperDetailController.querySessionMsg();\n    _replyContentController = _whisperDetailController.replyContentController;\n    _focuslistener();\n  }\n\n  _focuslistener() {\n    replyContentFocusNode.addListener(() {\n      if (replyContentFocusNode.hasFocus) {\n        toolbarType.value = 'input';\n      }\n    });\n  }\n\n  @override\n  void didChangeMetrics() {\n    super.didChangeMetrics();\n    final String routePath = Get.currentRoute;\n    if (mounted && routePath.startsWith('/whisperDetail')) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        // 键盘高度\n        final viewInsets = EdgeInsets.fromViewPadding(\n            View.of(context).viewInsets, View.of(context).devicePixelRatio);\n        _debouncer.run(() {\n          if (mounted) {\n            if (keyboardHeight == 0) {\n              setState(() {\n                keyboardHeight =\n                    keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;\n                if (keyboardHeight != 0) {\n                  emoteHeight = keyboardHeight;\n                }\n              });\n            }\n          }\n        });\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    replyContentFocusNode.removeListener(() {});\n    replyContentFocusNode.dispose();\n    super.dispose();\n  }\n\n  void onChooseEmote(PackageItem package, Emote emote) {\n    _whisperDetailController.emoteList.add(\n      {'text': emote.text, 'url': emote.url},\n    );\n    final int cursorPosition =\n        max(_replyContentController.selection.baseOffset, 0);\n    final String currentText = _replyContentController.text;\n    final String newText = currentText.substring(0, cursorPosition) +\n        emote.text! +\n        currentText.substring(cursorPosition);\n    _replyContentController.value = TextEditingValue(\n      text: newText,\n      selection:\n          TextSelection.collapsed(offset: cursorPosition + emote.text!.length),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        automaticallyImplyLeading: false,\n        title: SizedBox(\n          width: double.infinity,\n          height: 50,\n          child: Row(\n            children: [\n              SizedBox(\n                width: 34,\n                height: 34,\n                child: IconButton(\n                  onPressed: () => Get.back(),\n                  icon: Icon(\n                    Icons.arrow_back_ios,\n                    size: 18,\n                    color: Theme.of(context).colorScheme.onPrimaryContainer,\n                  ),\n                ),\n              ),\n              const SizedBox(width: 10),\n              GestureDetector(\n                onTap: () {\n                  feedBack();\n                  Get.toNamed(\n                    '/member?mid=${_whisperDetailController.mid}',\n                    arguments: {\n                      'face': _whisperDetailController.face,\n                      'heroTag': null\n                    },\n                  );\n                },\n                child: Row(\n                  children: <Widget>[\n                    Hero(\n                      tag: _whisperDetailController.heroTag,\n                      child: NetworkImgLayer(\n                        width: 34,\n                        height: 34,\n                        type: 'avatar',\n                        src: _whisperDetailController.face,\n                      ),\n                    ),\n                    const SizedBox(width: 10),\n                    Text(\n                      _whisperDetailController.name,\n                      style: Theme.of(context).textTheme.titleMedium,\n                    ),\n                  ],\n                ),\n              ),\n              const SizedBox(width: 36, height: 36),\n            ],\n          ),\n        ),\n        actions: [\n          PopupMenuButton(\n            icon: const Icon(Icons.more_vert_outlined, size: 20),\n            itemBuilder: (BuildContext context) => <PopupMenuEntry>[\n              PopupMenuItem(\n                onTap: () => _whisperDetailController.removeSession(context),\n                child: const Text('关闭会话'),\n              )\n            ],\n          ),\n          const SizedBox(width: 14)\n        ],\n      ),\n      body: Column(\n        children: [\n          Expanded(\n            child: GestureDetector(\n              onTap: () {\n                FocusScope.of(context).unfocus();\n                toolbarType.value = '';\n              },\n              child: FutureBuilder(\n                future: _futureBuilderFuture,\n                builder: (BuildContext context, snapshot) {\n                  if (snapshot.connectionState == ConnectionState.done) {\n                    if (snapshot.data == null) {\n                      return const SizedBox();\n                    }\n                    final Map data = snapshot.data as Map;\n                    if (data['status']) {\n                      List messageList = _whisperDetailController.messageList;\n                      return Obx(\n                        () => messageList.isEmpty\n                            ? const SizedBox()\n                            : Align(\n                                alignment: Alignment.topCenter,\n                                child: ListView.separated(\n                                  itemCount: messageList.length,\n                                  shrinkWrap: true,\n                                  reverse: true,\n                                  itemBuilder: (_, int i) {\n                                    return ChatItem(\n                                      item: messageList[i],\n                                      e_infos: _whisperDetailController.eInfos,\n                                      ctr: _whisperDetailController,\n                                    );\n                                  },\n                                  separatorBuilder: (_, int i) {\n                                    return i == 0\n                                        ? const SizedBox(height: 20)\n                                        : const SizedBox.shrink();\n                                  },\n                                ),\n                              ),\n                      );\n                    } else {\n                      // 请求错误\n                      return const SizedBox();\n                    }\n                  } else {\n                    // 骨架屏\n                    return const SizedBox();\n                  }\n                },\n              ),\n            ),\n          ),\n          Obx(\n            () => Container(\n              padding: EdgeInsets.fromLTRB(\n                8,\n                12,\n                12,\n                toolbarType.value == ''\n                    ? MediaQuery.of(context).padding.bottom + 6\n                    : 6,\n              ),\n              decoration: BoxDecoration(\n                border: Border(\n                  top: BorderSide(\n                    width: 1,\n                    color: Colors.grey.withOpacity(0.15),\n                  ),\n                ),\n              ),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  ToolbarIconButton(\n                    onPressed: () {\n                      if (toolbarType.value == '') {\n                        toolbarType.value = 'emote';\n                      } else if (toolbarType.value == 'input') {\n                        FocusScope.of(context).unfocus();\n                        toolbarType.value = 'emote';\n                      } else if (toolbarType.value == 'emote') {\n                        FocusScope.of(context).requestFocus();\n                      }\n                    },\n                    icon: const Icon(Icons.emoji_emotions_outlined, size: 22),\n                    toolbarType: toolbarType.value,\n                    selected: false,\n                  ),\n                  const SizedBox(width: 4),\n                  Expanded(\n                    child: Container(\n                      height: 45,\n                      decoration: BoxDecoration(\n                        color: Theme.of(context)\n                            .colorScheme\n                            .outline\n                            .withOpacity(0.05),\n                        borderRadius: BorderRadius.circular(40.0),\n                      ),\n                      child: TextField(\n                        style: Theme.of(context).textTheme.titleMedium,\n                        controller: _replyContentController,\n                        autofocus: false,\n                        focusNode: replyContentFocusNode,\n                        decoration: const InputDecoration(\n                          border: InputBorder.none, // 移除默认边框\n                          hintText: '文明发言 ～', // 提示文本\n                          contentPadding: EdgeInsets.symmetric(\n                              horizontal: 16.0, vertical: 12.0), // 内边距\n                        ),\n                      ),\n                    ),\n                  ),\n                  IconButton(\n                    onPressed: _whisperDetailController.sendMsg,\n                    icon: Icon(\n                      Icons.send,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Obx(\n            () => AnimatedSize(\n              curve: Curves.linear,\n              duration: const Duration(milliseconds: 200),\n              child: SizedBox(\n                width: double.infinity,\n                height: toolbarType.value == 'input'\n                    ? keyboardHeight\n                    : toolbarType.value == 'emote'\n                        ? emoteHeight\n                        : 0,\n                child: EmotePanel(\n                  onChoose: (package, emote) => onChooseEmote(package, emote),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n      resizeToAvoidBottomInset: false,\n    );\n  }\n}\n\ntypedef DebounceCallback = void Function();\n\nclass Debouncer {\n  DebounceCallback? callback;\n  final int? milliseconds;\n  Timer? _timer;\n\n  Debouncer({this.milliseconds});\n\n  run(DebounceCallback callback) {\n    if (_timer != null) {\n      _timer!.cancel();\n    }\n    _timer = Timer(Duration(milliseconds: milliseconds!), () {\n      callback();\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/whisper_detail/widget/chat_item.dart",
    "content": "// ignore_for_file: must_be_immutable\n// ignore_for_file: constant_identifier_names\n\nimport 'dart:convert';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';\nimport 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport 'package:pilipala/utils/utils.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport '../../../http/search.dart';\nimport '../controller.dart';\n\nenum MsgType {\n  invalid(value: 0, label: \"空空的~\"),\n  text(value: 1, label: \"文本消息\"),\n  pic(value: 2, label: \"图片消息\"),\n  audio(value: 3, label: \"语音消息\"),\n  share(value: 4, label: \"分享消息\"),\n  revoke(value: 5, label: \"撤回消息\"),\n  custom_face(value: 6, label: \"自定义表情\"),\n  share_v2(value: 7, label: \"分享v2消息\"),\n  sys_cancel(value: 8, label: \"系统撤销\"),\n  mini_program(value: 9, label: \"小程序\"),\n  notify_msg(value: 10, label: \"业务通知\"),\n  archive_card(value: 11, label: \"投稿卡片\"),\n  article_card(value: 12, label: \"专栏卡片\"),\n  pic_card(value: 13, label: \"图片卡片\"),\n  common_share(value: 14, label: \"异形卡片\"),\n  auto_reply_push(value: 16, label: \"自动回复推送\"),\n  notify_text(value: 18, label: \"文本提示\");\n\n  final int value;\n  final String label;\n  const MsgType({required this.value, required this.label});\n  static MsgType parse(int value) {\n    return MsgType.values\n        .firstWhere((e) => e.value == value, orElse: () => MsgType.invalid);\n  }\n}\n\nclass ChatItem extends StatelessWidget {\n  dynamic item;\n  List? e_infos;\n  WhisperDetailController ctr;\n\n  ChatItem({\n    super.key,\n    required this.item,\n    required this.ctr,\n    this.e_infos,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    bool isOwner =\n        item.senderUid == GStrorage.userInfo.get('userInfoCache').mid;\n\n    bool isPic = item.msgType == MsgType.pic.value; // 图片\n    bool isText = item.msgType == MsgType.text.value; // 文本\n    // bool isArchive = item.msgType == 11; // 投稿\n    // bool isArticle = item.msgType == 12; // 专栏\n    bool isRevoke = item.msgType == MsgType.revoke.value; // 撤回消息\n    bool isShareV2 = item.msgType == MsgType.share_v2.value;\n    bool isSystem = item.msgType == MsgType.notify_text.value ||\n        item.msgType == MsgType.notify_msg.value ||\n        item.msgType == MsgType.pic_card.value ||\n        item.msgType == MsgType.auto_reply_push.value;\n    dynamic content = item.content ?? '';\n    Color textColor(BuildContext context) {\n      return isOwner\n          ? Theme.of(context).colorScheme.onPrimary\n          : Theme.of(context).colorScheme.onBackground;\n    }\n\n    const double safeDistanceval = 6;\n    const double borderRadiusVal = 12;\n    const double paddingVal = 10;\n\n    Widget richTextMessage(BuildContext context) {\n      var text = content['content'];\n      if (e_infos != null) {\n        final List<InlineSpan> children = [];\n        Map<String, String> emojiMap = {};\n        for (var e in e_infos!) {\n          emojiMap[e['text']] = e['url'];\n        }\n        text.splitMapJoin(\n          RegExp(r\"\\[[^\\[\\]]+\\]\"),\n          onMatch: (Match match) {\n            final String emojiKey = match[0]!;\n            if (emojiMap.containsKey(emojiKey)) {\n              children.add(WidgetSpan(\n                child: NetworkImgLayer(\n                  width: 18,\n                  height: 18,\n                  src: emojiMap[emojiKey]!,\n                ),\n              ));\n            } else {\n              children.add(\n                TextSpan(\n                  text: emojiKey,\n                  style: TextStyle(\n                    color: textColor(context),\n                    letterSpacing: 0.6,\n                    height: 1.5,\n                  ),\n                ),\n              );\n            }\n            return '';\n          },\n          onNonMatch: (String text) {\n            children.add(TextSpan(\n                text: text,\n                style: TextStyle(\n                  color: textColor(context),\n                  letterSpacing: 0.6,\n                  height: 1.5,\n                )));\n            return '';\n          },\n        );\n        return SelectableText.rich(\n          TextSpan(\n            children: children,\n          ),\n        );\n      } else {\n        return SelectableText(\n          text,\n          style: TextStyle(\n            letterSpacing: 0.6,\n            color: textColor(context),\n            height: 1.5,\n          ),\n        );\n      }\n    }\n\n    Widget messageContent(BuildContext context) {\n      switch (MsgType.parse(item.msgType)) {\n        case MsgType.notify_msg:\n          return SystemNotice(item: item);\n        case MsgType.pic_card:\n          return SystemNotice2(item: item);\n        case MsgType.notify_text:\n          return SelectableText(\n            jsonDecode(content['content'])\n                .map((m) => m['text'] as String)\n                .join(\"\\n\"),\n            style: TextStyle(\n              letterSpacing: 0.6,\n              height: 5,\n              color: Theme.of(context).colorScheme.outline.withOpacity(0.8),\n            ),\n          );\n        case MsgType.text:\n          return richTextMessage(context);\n        case MsgType.pic:\n          return InkWell(\n            onTap: () {\n              Navigator.of(context).push(\n                HeroDialogRoute<void>(\n                  builder: (BuildContext context) => InteractiveviewerGallery(\n                    sources: ctr.picList,\n                    initIndex: ctr.picList.indexOf(content['url']),\n                    onPageChanged: (int pageIndex) {},\n                  ),\n                ),\n              );\n            },\n            child: NetworkImgLayer(\n              width: 220,\n              height: 220 * content['height'] / content['width'],\n              src: content['url'],\n            ),\n          );\n        case MsgType.share_v2:\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              GestureDetector(\n                onTap: () async {\n                  SmartDialog.showLoading();\n                  final String bvid = content[\"bvid\"];\n                  // 16番剧 5投稿\n                  final int source = content[\"source\"];\n                  final String? url = content[\"url\"];\n\n                  final int cid = await SearchHttp.ab2c(bvid: bvid);\n                  final String heroTag = Utils.makeHeroTag(bvid);\n                  await SmartDialog.dismiss();\n                  if (source == 5) {\n                    Get.toNamed<dynamic>(\n                      '/video?bvid=$bvid&cid=$cid',\n                      arguments: <String, String?>{\n                        'pic': content['thumb'],\n                        'heroTag': heroTag,\n                      },\n                    );\n                  }\n                  if (source == 16) {\n                    if (url != null) {\n                      final String area = url.split('/').last;\n                      if (area.startsWith('ep')) {\n                        RoutePush.bangumiPush(null, Utils.matchNum(area).first);\n                      } else if (area.startsWith('ss')) {\n                        RoutePush.bangumiPush(Utils.matchNum(area).first, null);\n                      }\n                    }\n                  }\n                },\n                child: NetworkImgLayer(\n                  width: 220,\n                  height: 220 * 9 / 16,\n                  src: content['thumb'],\n                ),\n              ),\n              const SizedBox(height: 6),\n              Text(\n                content['title'],\n                style: TextStyle(\n                  letterSpacing: 0.6,\n                  height: 1.5,\n                  color: textColor(context),\n                  fontWeight: FontWeight.bold,\n                ),\n              ),\n              const SizedBox(height: 1),\n              Text(\n                content['author'] ?? '',\n                style: TextStyle(\n                  letterSpacing: 0.6,\n                  height: 1.5,\n                  color: textColor(context).withOpacity(0.6),\n                  fontSize: 12,\n                ),\n              ),\n            ],\n          );\n        case MsgType.archive_card:\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              GestureDetector(\n                onTap: () async {\n                  SmartDialog.showLoading();\n                  var bvid = content[\"bvid\"];\n                  final int cid = await SearchHttp.ab2c(bvid: bvid);\n                  final String heroTag = Utils.makeHeroTag(bvid);\n                  SmartDialog.dismiss<dynamic>().then(\n                    (e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',\n                        arguments: <String, String?>{\n                          'pic': content['thumb'] ?? '',\n                          'heroTag': heroTag,\n                        }),\n                  );\n                },\n                child: NetworkImgLayer(\n                  width: 220,\n                  height: 220 * 9 / 16,\n                  src: content['cover'],\n                ),\n              ),\n              const SizedBox(height: 6),\n              Text(\n                content['title'],\n                style: TextStyle(\n                  letterSpacing: 0.6,\n                  height: 1.5,\n                  color: textColor(context),\n                  fontWeight: FontWeight.bold,\n                ),\n              ),\n              const SizedBox(height: 1),\n              Text(\n                Utils.timeFormat(content['times']),\n                style: TextStyle(\n                  letterSpacing: 0.6,\n                  height: 1.5,\n                  color: textColor(context).withOpacity(0.6),\n                  fontSize: 12,\n                ),\n              ),\n            ],\n          );\n        case MsgType.auto_reply_push:\n          return Container(\n            constraints: const BoxConstraints(\n              maxWidth: 300.0, // 设置最大宽度为200.0\n            ),\n            decoration: BoxDecoration(\n              color: Theme.of(context)\n                  .colorScheme\n                  .secondaryContainer\n                  .withOpacity(0.4),\n              borderRadius: const BorderRadius.all(\n                Radius.circular(16),\n              ),\n            ),\n            margin: const EdgeInsets.all(12),\n            padding: const EdgeInsets.all(12),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                  content['main_title'],\n                  style: TextStyle(\n                    letterSpacing: 0.6,\n                    height: 1.5,\n                    color: textColor(context),\n                    fontWeight: FontWeight.bold,\n                  ),\n                ),\n                for (var i in content['sub_cards']) ...<Widget>[\n                  const SizedBox(height: 6),\n                  GestureDetector(\n                    onTap: () async {\n                      RegExp bvRegex =\n                          RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);\n                      Iterable<Match> matches =\n                          bvRegex.allMatches(i['jump_url']);\n                      if (matches.isNotEmpty) {\n                        Match match = matches.first;\n                        String bvid = match.group(0)!;\n                        try {\n                          SmartDialog.showLoading();\n                          final int cid = await SearchHttp.ab2c(bvid: bvid);\n                          final String heroTag = Utils.makeHeroTag(bvid);\n                          SmartDialog.dismiss<dynamic>().then(\n                            (e) => Get.toNamed<dynamic>(\n                                '/video?bvid=$bvid&cid=$cid',\n                                arguments: <String, String?>{\n                                  'pic': i['cover_url'],\n                                  'heroTag': heroTag,\n                                }),\n                          );\n                        } catch (err) {\n                          SmartDialog.dismiss();\n                          SmartDialog.showToast(err.toString());\n                        }\n                      } else {\n                        SmartDialog.showToast('未匹配到 BV 号');\n                        Get.toNamed('/webview',\n                            arguments: {'url': i['jump_url']});\n                      }\n                    },\n                    child: Row(\n                      children: [\n                        NetworkImgLayer(\n                          width: 130,\n                          height: 130 * 9 / 16,\n                          src: i['cover_url'],\n                        ),\n                        const SizedBox(width: 6),\n                        Expanded(\n                            child: Column(\n                          crossAxisAlignment: CrossAxisAlignment.start,\n                          children: [\n                            Text(\n                              i['field1'],\n                              maxLines: 2,\n                              style: TextStyle(\n                                letterSpacing: 0.6,\n                                height: 1.5,\n                                color: textColor(context),\n                                fontWeight: FontWeight.bold,\n                              ),\n                            ),\n                            Text(\n                              i['field2'],\n                              style: TextStyle(\n                                letterSpacing: 0.6,\n                                height: 1.5,\n                                color: textColor(context).withOpacity(0.6),\n                                fontSize: 12,\n                              ),\n                            ),\n                            Text(\n                              i['field3'],\n                              style: TextStyle(\n                                letterSpacing: 0.6,\n                                height: 1.5,\n                                color: textColor(context).withOpacity(0.6),\n                                fontSize: 12,\n                              ),\n                            ),\n                          ],\n                        )),\n                      ],\n                    ),\n                  ),\n                ],\n              ],\n            ),\n          );\n        default:\n          return Text(\n            content != null && content != ''\n                ? (content['content'] ?? content.toString())\n                : '不支持的消息类型',\n            style: TextStyle(\n              letterSpacing: 0.6,\n              height: 1.5,\n              color: textColor(context),\n              fontWeight: FontWeight.bold,\n            ),\n          );\n      }\n    }\n\n    return isSystem\n        ? messageContent(context)\n        : isRevoke\n            ? const SizedBox()\n            : Container(\n                padding: const EdgeInsets.only(top: 6, bottom: 6),\n                decoration: BoxDecoration(\n                    border: Border(\n                  left: item.msgStatus == 1 && !isOwner\n                      ? BorderSide(\n                          width: 4, color: Theme.of(context).dividerColor)\n                      : BorderSide.none,\n                  right: item.msgStatus == 1 && isOwner\n                      ? BorderSide(\n                          width: 4, color: Theme.of(context).primaryColor)\n                      : BorderSide.none,\n                )),\n                child: Row(\n                  mainAxisAlignment: !isOwner\n                      ? MainAxisAlignment.start\n                      : MainAxisAlignment.end,\n                  crossAxisAlignment: CrossAxisAlignment.center,\n                  children: [\n                    const SizedBox(width: safeDistanceval),\n                    Container(\n                      constraints: const BoxConstraints(\n                        maxWidth: 300.0, // 设置最大宽度为200.0\n                      ),\n                      decoration: BoxDecoration(\n                        color: isOwner\n                            ? Theme.of(context)\n                                .colorScheme\n                                .primary\n                                .withAlpha(180)\n                            : Theme.of(context)\n                                .colorScheme\n                                .outlineVariant\n                                .withOpacity(0.6)\n                                .withAlpha(125),\n                        borderRadius: BorderRadius.only(\n                          topLeft: const Radius.circular(borderRadiusVal),\n                          topRight: const Radius.circular(borderRadiusVal),\n                          bottomLeft:\n                              Radius.circular(isOwner ? borderRadiusVal : 2),\n                          bottomRight:\n                              Radius.circular(isOwner ? 2 : borderRadiusVal),\n                        ),\n                      ),\n                      margin: const EdgeInsets.only(\n                        left: 8,\n                        right: 8,\n                      ),\n                      padding: const EdgeInsets.all(paddingVal),\n                      child: Column(\n                        crossAxisAlignment: isOwner\n                            ? CrossAxisAlignment.end\n                            : CrossAxisAlignment.start,\n                        children: [\n                          messageContent(context),\n                          SizedBox(height: isPic ? 7 : 4),\n                          Row(\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              Text(\n                                Utils.dateFormat(item.timestamp),\n                                style: Theme.of(context)\n                                    .textTheme\n                                    .labelSmall!\n                                    .copyWith(\n                                        color: isOwner\n                                            ? Theme.of(context)\n                                                .colorScheme\n                                                .onPrimary\n                                                .withOpacity(0.8)\n                                            : Theme.of(context)\n                                                .colorScheme\n                                                .onSecondaryContainer\n                                                .withOpacity(0.8)),\n                              ),\n                              item.msgStatus == 1\n                                  ? Text(\n                                      '  已撤回',\n                                      style: Theme.of(context)\n                                          .textTheme\n                                          .labelSmall!,\n                                    )\n                                  : const SizedBox()\n                            ],\n                          )\n                        ],\n                      ),\n                    ),\n                    const SizedBox(width: safeDistanceval),\n                  ],\n                ),\n              );\n  }\n}\n\nclass SystemNotice extends StatelessWidget {\n  dynamic item;\n  SystemNotice({super.key, this.item});\n\n  @override\n  Widget build(BuildContext context) {\n    Map content = item.content ?? '';\n    return Row(\n      children: [\n        const SizedBox(width: 12),\n        Container(\n          constraints: const BoxConstraints(\n            maxWidth: 300.0, // 设置最大宽度为200.0\n          ),\n          decoration: BoxDecoration(\n            color: Theme.of(context)\n                .colorScheme\n                .secondaryContainer\n                .withOpacity(0.4),\n            borderRadius: const BorderRadius.only(\n              topLeft: Radius.circular(16),\n              topRight: Radius.circular(16),\n              bottomLeft: Radius.circular(6),\n              bottomRight: Radius.circular(16),\n            ),\n          ),\n          margin: const EdgeInsets.only(top: 12),\n          padding: const EdgeInsets.all(12),\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Text(content['title'],\n                  style: Theme.of(context)\n                      .textTheme\n                      .titleMedium!\n                      .copyWith(fontWeight: FontWeight.bold)),\n              Text(\n                Utils.dateFormat(item.timestamp),\n                style: Theme.of(context)\n                    .textTheme\n                    .labelSmall!\n                    .copyWith(color: Theme.of(context).colorScheme.outline),\n              ),\n              Divider(\n                color: Theme.of(context).colorScheme.primary.withOpacity(0.05),\n              ),\n              SelectableText(\n                content['text'],\n              )\n            ],\n          ),\n        ),\n        const Spacer(),\n      ],\n    );\n  }\n}\n\nclass SystemNotice2 extends StatelessWidget {\n  dynamic item;\n  SystemNotice2({super.key, this.item});\n\n  @override\n  Widget build(BuildContext context) {\n    Map content = item.content ?? '';\n    return Row(\n      children: [\n        const SizedBox(width: 12),\n        Container(\n          constraints: const BoxConstraints(\n            maxWidth: 300.0, // 设置最大宽度为200.0\n          ),\n          margin: const EdgeInsets.only(top: 12),\n          padding: const EdgeInsets.only(bottom: 6),\n          child: NetworkImgLayer(\n            width: 320,\n            height: 150,\n            src: content['pic_url'],\n          ),\n        ),\n        const Spacer(),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_gallery/custom_dismissible.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// A widget used to dismiss its [child].\n///\n/// Similar to [Dismissible] with some adjustments.\nclass CustomDismissible extends StatefulWidget {\n  const CustomDismissible({\n    required this.child,\n    this.onDismissed,\n    this.dismissThreshold = 0.2,\n    this.enabled = true,\n    Key? key,\n  }) : super(key: key);\n\n  final Widget child;\n  final double dismissThreshold;\n  final VoidCallback? onDismissed;\n  final bool enabled;\n\n  @override\n  State<CustomDismissible> createState() => _CustomDismissibleState();\n}\n\nclass _CustomDismissibleState extends State<CustomDismissible>\n    with SingleTickerProviderStateMixin {\n  late AnimationController _animateController;\n  late Animation<Offset> _moveAnimation;\n  late Animation<double> _scaleAnimation;\n  late Animation<Decoration> _opacityAnimation;\n\n  double _dragExtent = 0;\n  bool _dragUnderway = false;\n\n  bool get _isActive => _dragUnderway || _animateController.isAnimating;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _animateController = AnimationController(\n      duration: const Duration(milliseconds: 300),\n      vsync: this,\n    );\n\n    _updateMoveAnimation();\n  }\n\n  @override\n  void dispose() {\n    _animateController.dispose();\n\n    super.dispose();\n  }\n\n  void _updateMoveAnimation() {\n    final double end = _dragExtent.sign;\n\n    _moveAnimation = _animateController.drive(\n      Tween<Offset>(\n        begin: Offset.zero,\n        end: Offset(0, end),\n      ),\n    );\n\n    _scaleAnimation = _animateController.drive(Tween<double>(\n      begin: 1,\n      end: 0.5,\n    ));\n\n    _opacityAnimation = DecorationTween(\n      begin: const BoxDecoration(color: Color(0xFF000000)),\n      end: const BoxDecoration(color: Color(0x00000000)),\n    ).animate(_animateController);\n  }\n\n  void _handleDragStart(DragStartDetails details) {\n    _dragUnderway = true;\n\n    if (_animateController.isAnimating) {\n      _dragExtent =\n          _animateController.value * context.size!.height * _dragExtent.sign;\n      _animateController.stop();\n    } else {\n      _dragExtent = 0.0;\n      _animateController.value = 0.0;\n    }\n    setState(_updateMoveAnimation);\n  }\n\n  void _handleDragUpdate(DragUpdateDetails details) {\n    if (!_isActive || _animateController.isAnimating) {\n      return;\n    }\n\n    final double delta = details.primaryDelta!;\n    final double oldDragExtent = _dragExtent;\n\n    if (_dragExtent + delta < 0) {\n      _dragExtent += delta;\n    } else if (_dragExtent + delta > 0) {\n      _dragExtent += delta;\n    }\n\n    if (oldDragExtent.sign != _dragExtent.sign) {\n      setState(_updateMoveAnimation);\n    }\n\n    if (!_animateController.isAnimating) {\n      _animateController.value = _dragExtent.abs() / context.size!.height;\n    }\n  }\n\n  void _handleDragEnd(DragEndDetails details) {\n    if (!_isActive || _animateController.isAnimating) {\n      return;\n    }\n\n    _dragUnderway = false;\n\n    if (_animateController.isCompleted) {\n      return;\n    }\n\n    if (!_animateController.isDismissed) {\n      // if the dragged value exceeded the dismissThreshold, call onDismissed\n      // else animate back to initial position.\n      if (_animateController.value > widget.dismissThreshold) {\n        widget.onDismissed?.call();\n      } else {\n        _animateController.reverse();\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final Widget content = DecoratedBoxTransition(\n      decoration: _opacityAnimation,\n      child: SlideTransition(\n        position: _moveAnimation,\n        child: ScaleTransition(\n          scale: _scaleAnimation,\n          child: widget.child,\n        ),\n      ),\n    );\n\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onVerticalDragStart: widget.enabled ? _handleDragStart : null,\n      onVerticalDragUpdate: widget.enabled ? _handleDragUpdate : null,\n      onVerticalDragEnd: widget.enabled ? _handleDragEnd : null,\n      child: content,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_gallery/hero_dialog_route.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// A [PageRoute] with a semi transparent background.\n///\n/// Similar to calling [showDialog] except it can be used with a [Navigator] to\n/// show a [Hero] animation.\nclass HeroDialogRoute<T> extends PageRoute<T> {\n  HeroDialogRoute({\n    required this.builder,\n    this.onBackgroundTap,\n  }) : super();\n\n  final WidgetBuilder builder;\n\n  /// Called when the background is tapped.\n  final VoidCallback? onBackgroundTap;\n\n  @override\n  bool get opaque => false;\n\n  @override\n  bool get barrierDismissible => true;\n\n  @override\n  String? get barrierLabel => null;\n\n  @override\n  Duration get transitionDuration => const Duration(milliseconds: 300);\n\n  @override\n  bool get maintainState => true;\n\n  @override\n  Color? get barrierColor => null;\n\n  @override\n  Widget buildTransitions(\n    BuildContext context,\n    Animation<double> animation,\n    Animation<double> secondaryAnimation,\n    Widget child,\n  ) {\n    return FadeTransition(\n      opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),\n      child: child,\n    );\n  }\n\n  @override\n  Widget buildPage(\n    BuildContext context,\n    Animation<double> animation,\n    Animation<double> secondaryAnimation,\n  ) {\n    final Widget child = builder(context);\n    final Widget result = Semantics(\n      scopesRoute: true,\n      explicitChildNodes: true,\n      child: child,\n    );\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_gallery/index.dart",
    "content": "library pl_gallery;\n\nexport './hero_dialog_route.dart';\nexport './custom_dismissible.dart';\nexport './interactiveviewer_gallery.dart';\nexport './interactive_viewer_boundary.dart';\n"
  },
  {
    "path": "lib/plugin/pl_gallery/interactive_viewer_boundary.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// A callback for the [InteractiveViewerBoundary] that is called when the scale\n/// changed.\ntypedef ScaleChanged = void Function(double scale);\n\n/// Builds an [InteractiveViewer] and provides callbacks that are called when a\n/// horizontal boundary has been hit.\n///\n/// The callbacks are called when an interaction ends by listening to the\n/// [InteractiveViewer.onInteractionEnd] callback.\nclass InteractiveViewerBoundary extends StatefulWidget {\n  const InteractiveViewerBoundary({\n    required this.child,\n    required this.boundaryWidth,\n    this.controller,\n    this.onScaleChanged,\n    this.onLeftBoundaryHit,\n    this.onRightBoundaryHit,\n    this.onNoBoundaryHit,\n    this.maxScale,\n    this.minScale,\n    Key? key,\n  }) : super(key: key);\n\n  final Widget child;\n\n  /// The max width this widget can have.\n  ///\n  /// If the [InteractiveViewer] can take up the entire screen width, this\n  /// should be set to `MediaQuery.of(context).size.width`.\n  final double boundaryWidth;\n\n  /// The [TransformationController] for the [InteractiveViewer].\n  final TransformationController? controller;\n\n  /// Called when the scale changed after an interaction ended.\n  final ScaleChanged? onScaleChanged;\n\n  /// Called when the left boundary has been hit after an interaction ended.\n  final VoidCallback? onLeftBoundaryHit;\n\n  /// Called when the right boundary has been hit after an interaction ended.\n  final VoidCallback? onRightBoundaryHit;\n\n  /// Called when no boundary has been hit after an interaction ended.\n  final VoidCallback? onNoBoundaryHit;\n\n  final double? maxScale;\n\n  final double? minScale;\n\n  @override\n  InteractiveViewerBoundaryState createState() =>\n      InteractiveViewerBoundaryState();\n}\n\nclass InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary> {\n  TransformationController? _controller;\n\n  double? _scale;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _controller = widget.controller ?? TransformationController();\n  }\n\n  @override\n  void dispose() {\n    _controller!.dispose();\n\n    super.dispose();\n  }\n\n  void _updateBoundaryDetection() {\n    final double scale = _controller!.value.row0[0];\n\n    if (_scale != scale) {\n      // the scale changed\n      _scale = scale;\n      widget.onScaleChanged?.call(scale);\n    }\n\n    if (scale <= 1.01) {\n      // cant hit any boundaries when the child is not scaled\n      return;\n    }\n\n    final double xOffset = _controller!.value.row0[3];\n    final double boundaryWidth = widget.boundaryWidth;\n    final double boundaryEnd = boundaryWidth * scale;\n    final double xPos = boundaryEnd + xOffset;\n\n    if (boundaryEnd.round() == xPos.round()) {\n      // left boundary hit\n      widget.onLeftBoundaryHit?.call();\n    } else if (boundaryWidth.round() == xPos.round()) {\n      // right boundary hit\n      widget.onRightBoundaryHit?.call();\n    } else {\n      widget.onNoBoundaryHit?.call();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return InteractiveViewer(\n      maxScale: widget.maxScale!,\n      minScale: widget.minScale!,\n      transformationController: _controller,\n      onInteractionEnd: (_) => _updateBoundaryDetection(),\n      child: widget.child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_gallery/interactiveviewer_gallery.dart",
    "content": "library interactiveviewer_gallery;\n\nimport 'dart:io';\n\nimport 'package:cached_network_image/cached_network_image.dart';\nimport 'package:dio/dio.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:pilipala/utils/download.dart';\nimport 'package:share_plus/share_plus.dart';\nimport 'package:status_bar_control/status_bar_control.dart';\nimport 'custom_dismissible.dart';\nimport 'interactive_viewer_boundary.dart';\n\n/// Builds a carousel controlled by a [PageView] for the tweet media sources.\n///\n/// Used for showing a full screen view of the [TweetMedia] sources.\n///\n/// The sources can be panned and zoomed interactively using an\n/// [InteractiveViewer].\n/// An [InteractiveViewerBoundary] is used to detect when the boundary of the\n/// source is hit after zooming in to disable or enable the swiping gesture of\n/// the [PageView].\n///\ntypedef IndexedFocusedWidgetBuilder = Widget Function(\n    BuildContext context, int index, bool isFocus, bool enablePageView);\n\ntypedef IndexedTagStringBuilder = String Function(int index);\n\nclass InteractiveviewerGallery<T> extends StatefulWidget {\n  const InteractiveviewerGallery({\n    required this.sources,\n    required this.initIndex,\n    this.itemBuilder,\n    this.maxScale = 4.5,\n    this.minScale = 1.0,\n    this.onPageChanged,\n    this.onDismissed,\n    Key? key,\n  }) : super(key: key);\n\n  /// The sources to show.\n  final List<T> sources;\n\n  /// The index of the first source in [sources] to show.\n  final int initIndex;\n\n  /// The item content\n  final IndexedFocusedWidgetBuilder? itemBuilder;\n\n  final double maxScale;\n\n  final double minScale;\n\n  final ValueChanged<int>? onPageChanged;\n\n  final ValueChanged<int>? onDismissed;\n\n  @override\n  State<InteractiveviewerGallery> createState() =>\n      _InteractiveviewerGalleryState();\n}\n\nclass _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>\n    with SingleTickerProviderStateMixin {\n  PageController? _pageController;\n  TransformationController? _transformationController;\n\n  /// The controller to animate the transformation value of the\n  /// [InteractiveViewer] when it should reset.\n  late AnimationController _animationController;\n  Animation<Matrix4>? _animation;\n\n  /// `true` when an source is zoomed in and not at the at a horizontal boundary\n  /// to disable the [PageView].\n  bool _enablePageView = true;\n\n  /// `true` when an source is zoomed in to disable the [CustomDismissible].\n  bool _enableDismiss = true;\n\n  late Offset _doubleTapLocalPosition;\n\n  int? currentIndex;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _pageController = PageController(initialPage: widget.initIndex);\n\n    _transformationController = TransformationController();\n\n    _animationController = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n    )\n      ..addListener(() {\n        _transformationController!.value =\n            _animation?.value ?? Matrix4.identity();\n      })\n      ..addStatusListener((AnimationStatus status) {\n        if (status == AnimationStatus.completed && !_enableDismiss) {\n          setState(() {\n            _enableDismiss = true;\n          });\n        }\n      });\n\n    currentIndex = widget.initIndex;\n    setStatusBar();\n  }\n\n  setStatusBar() async {\n    if (Platform.isIOS || Platform.isAndroid) {\n      await StatusBarControl.setHidden(true,\n          animation: StatusBarAnimation.FADE);\n    }\n  }\n\n  @override\n  void dispose() {\n    _pageController!.dispose();\n    _animationController.dispose();\n    try {\n      StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);\n    } catch (_) {}\n    super.dispose();\n  }\n\n  /// When the source gets scaled up, the swipe up / down to dismiss gets\n  /// disabled.\n  ///\n  /// When the scale resets, the dismiss and the page view swiping gets enabled.\n  void _onScaleChanged(double scale) {\n    final bool initialScale = scale <= widget.minScale;\n\n    if (initialScale) {\n      if (!_enableDismiss) {\n        setState(() {\n          _enableDismiss = true;\n        });\n      }\n\n      if (!_enablePageView) {\n        setState(() {\n          _enablePageView = true;\n        });\n      }\n    } else {\n      if (_enableDismiss) {\n        setState(() {\n          _enableDismiss = false;\n        });\n      }\n\n      if (_enablePageView) {\n        setState(() {\n          _enablePageView = false;\n        });\n      }\n    }\n  }\n\n  /// When the left boundary has been hit after scaling up the source, the page\n  /// view swiping gets enabled if it has a page to swipe to.\n  void _onLeftBoundaryHit() {\n    if (!_enablePageView && _pageController!.page!.floor() > 0) {\n      setState(() {\n        _enablePageView = true;\n      });\n    }\n  }\n\n  /// When the right boundary has been hit after scaling up the source, the page\n  /// view swiping gets enabled if it has a page to swipe to.\n  void _onRightBoundaryHit() {\n    if (!_enablePageView &&\n        _pageController!.page!.floor() < widget.sources.length - 1) {\n      setState(() {\n        _enablePageView = true;\n      });\n    }\n  }\n\n  /// When the source has been scaled up and no horizontal boundary has been hit,\n  /// the page view swiping gets disabled.\n  void _onNoBoundaryHit() {\n    if (_enablePageView) {\n      setState(() {\n        _enablePageView = false;\n      });\n    }\n  }\n\n  /// When the page view changed its page, the source will animate back into the\n  /// original scale if it was scaled up.\n  ///\n  /// Additionally the swipe up / down to dismiss gets enabled.\n  void _onPageChanged(int page) {\n    setState(() {\n      currentIndex = page;\n    });\n    widget.onPageChanged?.call(page);\n    if (_transformationController!.value != Matrix4.identity()) {\n      // animate the reset for the transformation of the interactive viewer\n\n      _animation = Matrix4Tween(\n        begin: _transformationController!.value,\n        end: Matrix4.identity(),\n      ).animate(\n        CurveTween(curve: Curves.easeOut).animate(_animationController),\n      );\n\n      _animationController.forward(from: 0);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return InteractiveViewerBoundary(\n      controller: _transformationController,\n      boundaryWidth: MediaQuery.of(context).size.width,\n      onScaleChanged: _onScaleChanged,\n      onLeftBoundaryHit: _onLeftBoundaryHit,\n      onRightBoundaryHit: _onRightBoundaryHit,\n      onNoBoundaryHit: _onNoBoundaryHit,\n      maxScale: widget.maxScale,\n      minScale: widget.minScale,\n      child: Stack(children: [\n        CustomDismissible(\n          onDismissed: () {\n            Navigator.of(context).pop();\n            widget.onDismissed?.call(_pageController!.page!.floor());\n          },\n          enabled: _enableDismiss,\n          child: PageView.builder(\n            onPageChanged: _onPageChanged,\n            controller: _pageController,\n            physics:\n                _enablePageView ? null : const NeverScrollableScrollPhysics(),\n            itemCount: widget.sources.length,\n            itemBuilder: (BuildContext context, int index) {\n              return GestureDetector(\n                onDoubleTapDown: (TapDownDetails details) {\n                  _doubleTapLocalPosition = details.localPosition;\n                },\n                onDoubleTap: onDoubleTap,\n                onLongPress: onLongPress,\n                child: widget.itemBuilder != null\n                    ? widget.itemBuilder!(\n                        context,\n                        index,\n                        index == currentIndex,\n                        _enablePageView,\n                      )\n                    : _itemBuilder(widget.sources, index),\n              );\n            },\n          ),\n        ),\n        Positioned(\n          bottom: 0,\n          left: 0,\n          right: 0,\n          child: Container(\n            padding: EdgeInsets.fromLTRB(\n                12, 8, 20, MediaQuery.of(context).padding.bottom + 8),\n            decoration: _enablePageView\n                ? BoxDecoration(\n                    gradient: LinearGradient(\n                      begin: Alignment.topCenter,\n                      end: Alignment.bottomCenter,\n                      colors: [\n                        Colors.transparent,\n                        Colors.black.withOpacity(0.3)\n                      ],\n                    ),\n                  )\n                : null,\n            child: Row(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                IconButton(\n                  icon: const Icon(Icons.close, color: Colors.white),\n                  onPressed: () {\n                    Navigator.of(context).pop();\n                    widget.onDismissed?.call(_pageController!.page!.floor());\n                  },\n                ),\n                widget.sources.length > 1\n                    ? Text(\n                        \"${currentIndex! + 1}/${widget.sources.length}\",\n                        style: const TextStyle(color: Colors.white),\n                      )\n                    : const SizedBox(),\n                PopupMenuButton(\n                  itemBuilder: (context) {\n                    return [\n                      PopupMenuItem(\n                        value: 0,\n                        onTap: () => onShareImg(widget.sources[currentIndex!]),\n                        child: const Text(\"分享图片\"),\n                      ),\n                      PopupMenuItem(\n                        value: 1,\n                        onTap: () {\n                          onCopyImg(widget.sources[currentIndex!].toString());\n                        },\n                        child: const Text(\"复制图片\"),\n                      ),\n                      PopupMenuItem(\n                        value: 2,\n                        onTap: () {\n                          DownloadUtils.downloadImg(\n                              widget.sources[currentIndex!]);\n                        },\n                        child: const Text(\"保存图片\"),\n                      ),\n                    ];\n                  },\n                  child: const Icon(Icons.more_horiz, color: Colors.white),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ]),\n    );\n  }\n\n  // 图片分享\n  void onShareImg(String imgUrl) async {\n    SmartDialog.showLoading();\n    var response = await Dio()\n        .get(imgUrl, options: Options(responseType: ResponseType.bytes));\n    final temp = await getTemporaryDirectory();\n    SmartDialog.dismiss();\n    String imgName =\n        \"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg\";\n    var path = '${temp.path}/$imgName';\n    File(path).writeAsBytesSync(response.data);\n    Share.shareXFiles([XFile(path)], subject: imgUrl);\n  }\n\n  // 复制图片\n  void onCopyImg(String imgUrl) {\n    Clipboard.setData(\n            ClipboardData(text: widget.sources[currentIndex!].toString()))\n        .then((value) {\n      SmartDialog.showToast('已复制到粘贴板');\n    }).catchError((err) {\n      SmartDialog.showNotify(\n        msg: err.toString(),\n        notifyType: NotifyType.error,\n      );\n    });\n  }\n\n  Widget _itemBuilder(sources, index) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () {\n        if (_enablePageView) {\n          Navigator.of(context).pop();\n        }\n      },\n      child: Center(\n        child: Hero(\n          tag: sources[index],\n          child: CachedNetworkImage(\n            fadeInDuration: const Duration(milliseconds: 0),\n            imageUrl: sources[index],\n            fit: BoxFit.contain,\n          ),\n        ),\n      ),\n    );\n  }\n\n  onDoubleTap() {\n    Matrix4 matrix = _transformationController!.value.clone();\n    double currentScale = matrix.row0.x;\n\n    double targetScale = widget.minScale;\n\n    if (currentScale <= widget.minScale) {\n      targetScale = widget.maxScale * 0.7;\n    }\n\n    double offSetX = targetScale == 1.0\n        ? 0.0\n        : -_doubleTapLocalPosition.dx * (targetScale - 1);\n    double offSetY = targetScale == 1.0\n        ? 0.0\n        : -_doubleTapLocalPosition.dy * (targetScale - 1);\n\n    matrix = Matrix4.fromList([\n      targetScale,\n      matrix.row1.x,\n      matrix.row2.x,\n      matrix.row3.x,\n      matrix.row0.y,\n      targetScale,\n      matrix.row2.y,\n      matrix.row3.y,\n      matrix.row0.z,\n      matrix.row1.z,\n      targetScale,\n      matrix.row3.z,\n      offSetX,\n      offSetY,\n      matrix.row2.w,\n      matrix.row3.w\n    ]);\n\n    _animation = Matrix4Tween(\n      begin: _transformationController!.value,\n      end: matrix,\n    ).animate(\n      CurveTween(curve: Curves.easeOut).animate(_animationController),\n    );\n    _animationController\n        .forward(from: 0)\n        .whenComplete(() => _onScaleChanged(targetScale));\n  }\n\n  onLongPress() {\n    showModalBottomSheet(\n      context: context,\n      useRootNavigator: true,\n      isScrollControlled: true,\n      builder: (context) {\n        return Container(\n          padding:\n              EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              InkWell(\n                onTap: () => Get.back(),\n                child: Container(\n                  height: 35,\n                  padding: const EdgeInsets.only(bottom: 2),\n                  child: Center(\n                    child: Container(\n                      width: 32,\n                      height: 3,\n                      decoration: BoxDecoration(\n                          color: Theme.of(context).colorScheme.outline,\n                          borderRadius:\n                              const BorderRadius.all(Radius.circular(3))),\n                    ),\n                  ),\n                ),\n              ),\n              ListTile(\n                onTap: () {\n                  onShareImg(widget.sources[currentIndex!]);\n                  Navigator.of(context).pop();\n                },\n                title: const Text('分享图片'),\n              ),\n              ListTile(\n                onTap: () {\n                  onCopyImg(widget.sources[currentIndex!].toString());\n                  Navigator.of(context).pop();\n                },\n                title: const Text('复制图片'),\n              ),\n              ListTile(\n                onTap: () {\n                  DownloadUtils.downloadImg(widget.sources[currentIndex!]);\n                  Navigator.of(context).pop();\n                },\n                title: const Text('保存图片'),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/controller.dart",
    "content": "// ignore_for_file: avoid_print\n\nimport 'dart:async';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_volume_controller/flutter_volume_controller.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:media_kit/media_kit.dart';\nimport 'package:media_kit_video/media_kit_video.dart';\nimport 'package:ns_danmaku/ns_danmaku.dart';\nimport 'package:pilipala/http/video.dart';\nimport 'package:pilipala/models/video/play/ao_output.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/plugin/pl_player/models/play_repeat.dart';\nimport 'package:pilipala/services/service_locator.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/global_data_cache.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:screen_brightness/screen_brightness.dart';\nimport 'package:status_bar_control/status_bar_control.dart';\nimport 'package:universal_platform/universal_platform.dart';\nimport '../../models/video/subTitile/content.dart';\nimport '../../models/video/subTitile/result.dart';\n// import 'package:wakelock_plus/wakelock_plus.dart';\n\nBox videoStorage = GStrorage.video;\nBox setting = GStrorage.setting;\nBox localCache = GStrorage.localCache;\n\nclass PlPlayerController {\n  Player? _videoPlayerController;\n  VideoController? _videoController;\n\n  // 添加一个私有静态变量来保存实例\n  static PlPlayerController? _instance;\n\n  // 流事件  监听播放状态变化\n  StreamSubscription? _playerEventSubs;\n\n  /// [playerStatus] has a [status] observable\n  final PlPlayerStatus playerStatus = PlPlayerStatus();\n\n  ///\n  final PlPlayerDataStatus dataStatus = PlPlayerDataStatus();\n\n  // bool controlsEnabled = false;\n\n  /// 响应数据\n  /// 带有Seconds的变量只在秒数更新时更新，以避免频繁触发重绘\n  // 播放位置\n  final Rx<Duration> _position = Rx(Duration.zero);\n  final RxInt positionSeconds = 0.obs;\n  final Rx<Duration> _sliderPosition = Rx(Duration.zero);\n  final RxInt sliderPositionSeconds = 0.obs;\n  // 展示使用\n  final Rx<Duration> _sliderTempPosition = Rx(Duration.zero);\n  final Rx<Duration> _duration = Rx(Duration.zero);\n  final RxInt durationSeconds = 0.obs;\n  final Rx<Duration> _buffered = Rx(Duration.zero);\n  final RxInt bufferedSeconds = 0.obs;\n\n  final Rx<int> _playerCount = Rx(0);\n\n  final Rx<double> _playbackSpeed = 1.0.obs;\n  final Rx<double> _longPressSpeed = 2.0.obs;\n  final Rx<double> _currentVolume = 1.0.obs;\n  final Rx<double> _currentBrightness = 0.0.obs;\n\n  final Rx<bool> _mute = false.obs;\n  final Rx<bool> _showControls = false.obs;\n  final Rx<bool> _showVolumeStatus = false.obs;\n  final Rx<bool> _showBrightnessStatus = false.obs;\n  final Rx<bool> _doubleSpeedStatus = false.obs;\n  final Rx<bool> _controlsLock = false.obs;\n  final Rx<bool> _isFullScreen = false.obs;\n  final Rx<bool> _subTitleOpen = false.obs;\n  final Rx<int> _subTitleCode = (-1).obs;\n  // 默认投稿视频格式\n  static Rx<String> _videoType = 'archive'.obs;\n\n  final Rx<String> _direction = 'horizontal'.obs;\n\n  Rx<bool> videoFitChanged = false.obs;\n  final Rx<BoxFit> _videoFit = Rx(BoxFit.contain);\n  final Rx<String> _videoFitDesc = Rx('包含');\n\n  ///\n  // ignore: prefer_final_fields\n  Rx<bool> _isSliderMoving = false.obs;\n  PlaylistMode _looping = PlaylistMode.none;\n  bool _autoPlay = false;\n  final bool _listenersInitialized = false;\n\n  // 记录历史记录\n  String _bvid = '';\n  int _cid = 0;\n  int _heartDuration = 0;\n  bool _enableHeart = true;\n  bool _isFirstTime = true;\n\n  Timer? _timer;\n  Timer? _timerForSeek;\n  Timer? _timerForVolume;\n  Timer? _timerForShowingVolume;\n  Timer? _timerForGettingVolume;\n  Timer? timerForTrackingMouse;\n  Timer? videoFitChangedTimer;\n\n  // final Durations durations;\n\n  List<Map<String, dynamic>> videoFitType = [\n    {'attr': BoxFit.contain, 'desc': '包含'},\n    {'attr': BoxFit.cover, 'desc': '覆盖'},\n    {'attr': BoxFit.fill, 'desc': '填充'},\n    {'attr': BoxFit.fitHeight, 'desc': '高度适应'},\n    {'attr': BoxFit.fitWidth, 'desc': '宽度适应'},\n    {'attr': BoxFit.scaleDown, 'desc': '缩小适应'},\n  ];\n\n  PreferredSizeWidget? headerControl;\n  PreferredSizeWidget? bottomControl;\n  Widget? danmuWidget;\n  RxList subtitles = [].obs;\n  String videoType = 'archive';\n\n  /// 数据加载监听\n  Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;\n\n  /// 播放状态监听\n  Stream<PlayerStatus> get onPlayerStatusChanged => playerStatus.status.stream;\n\n  /// 视频时长\n  Rx<Duration> get duration => _duration;\n  Stream<Duration> get onDurationChanged => _duration.stream;\n\n  /// 视频当前播放位置\n  Rx<Duration> get position => _position;\n  Stream<Duration> get onPositionChanged => _position.stream;\n\n  /// 视频播放速度\n  double get playbackSpeed => _playbackSpeed.value;\n\n  // 长按倍速\n  double get longPressSpeed => _longPressSpeed.value;\n\n  /// 视频缓冲\n  Rx<Duration> get buffered => _buffered;\n  Stream<Duration> get onBufferedChanged => _buffered.stream;\n\n  // 视频静音\n  Rx<bool> get mute => _mute;\n  Stream<bool> get onMuteChanged => _mute.stream;\n\n  /// 字幕开启状态\n  Rx<bool> get subTitleOpen => _subTitleOpen;\n  Rx<int> get subTitleCode => _subTitleCode;\n  // Stream<bool> get onSubTitleOpenChanged => _subTitleOpen.stream;\n\n  /// [videoPlayerController] instace of Player\n  Player? get videoPlayerController => _videoPlayerController;\n\n  /// [videoController] instace of Player\n  VideoController? get videoController => _videoController;\n\n  Rx<bool> get isSliderMoving => _isSliderMoving;\n\n  /// 进度条位置及监听\n  Rx<Duration> get sliderPosition => _sliderPosition;\n  Stream<Duration> get onSliderPositionChanged => _sliderPosition.stream;\n\n  Rx<Duration> get sliderTempPosition => _sliderTempPosition;\n  // Stream<Duration> get onSliderPositionChanged => _sliderPosition.stream;\n\n  /// 是否展示控制条及监听\n  Rx<bool> get showControls => _showControls;\n  Stream<bool> get onShowControlsChanged => _showControls.stream;\n\n  /// 音量控制条展示/隐藏\n  Rx<bool> get showVolumeStatus => _showVolumeStatus;\n  Stream<bool> get onShowVolumeStatusChanged => _showVolumeStatus.stream;\n\n  /// 亮度控制条展示/隐藏\n  Rx<bool> get showBrightnessStatus => _showBrightnessStatus;\n  Stream<bool> get onShowBrightnessStatusChanged =>\n      _showBrightnessStatus.stream;\n\n  /// 音量控制条\n  Rx<double> get volume => _currentVolume;\n  Stream<double> get onVolumeChanged => _currentVolume.stream;\n\n  /// 亮度控制条\n  Rx<double> get brightness => _currentBrightness;\n  Stream<double> get onBrightnessChanged => _currentBrightness.stream;\n\n  /// 是否循环\n  PlaylistMode get looping => _looping;\n\n  /// 是否自动播放\n  bool get autoplay => _autoPlay;\n\n  /// 视频比例\n  Rx<BoxFit> get videoFit => _videoFit;\n  Rx<String> get videoFitDEsc => _videoFitDesc;\n\n  /// 是否长按倍速\n  Rx<bool> get doubleSpeedStatus => _doubleSpeedStatus;\n\n  Rx<bool> isBuffering = true.obs;\n\n  /// 屏幕锁 为true时，关闭控制栏\n  Rx<bool> get controlsLock => _controlsLock;\n\n  /// 全屏状态\n  Rx<bool> get isFullScreen => _isFullScreen;\n\n  /// 全屏方向\n  Rx<String> get direction => _direction;\n\n  Rx<int> get playerCount => _playerCount;\n\n  ///\n  // Rx<String> get videoType => _videoType;\n\n  /// 弹幕开关\n  Rx<bool> isOpenDanmu = false.obs;\n  // 关联弹幕控制器\n  DanmakuController? danmakuController;\n  // 弹幕相关配置\n  late List blockTypes;\n  late double showArea;\n  late double opacityVal;\n  late double fontSizeVal;\n  late double strokeWidth;\n  late double danmakuDurationVal;\n  late List<double> speedsList;\n  // 缓存\n  double? defaultDuration;\n  late bool enableAutoLongPressSpeed = false;\n\n  // 播放顺序相关\n  PlayRepeat playRepeat = PlayRepeat.pause;\n\n  RxList<SubTitileContentModel> subtitleContents =\n      <SubTitileContentModel>[].obs;\n  RxString subtitleContent = ''.obs;\n\n  void updateSliderPositionSecond() {\n    int newSecond = _sliderPosition.value.inSeconds;\n    if (sliderPositionSeconds.value != newSecond) {\n      sliderPositionSeconds.value = newSecond;\n    }\n  }\n\n  void updatePositionSecond() {\n    int newSecond = _position.value.inSeconds;\n    if (positionSeconds.value != newSecond) {\n      positionSeconds.value = newSecond;\n    }\n  }\n\n  void updateDurationSecond() {\n    int newSecond = _duration.value.inSeconds;\n    if (durationSeconds.value != newSecond) {\n      durationSeconds.value = newSecond;\n    }\n  }\n\n  void updateBufferedSecond() {\n    int newSecond = _buffered.value.inSeconds;\n    if (bufferedSeconds.value != newSecond) {\n      bufferedSeconds.value = newSecond;\n    }\n  }\n\n  // 添加一个私有构造函数\n  PlPlayerController._internal(this.videoType) {\n    final cache = GlobalDataCache();\n    isOpenDanmu.value = cache.isOpenDanmu;\n    blockTypes = cache.blockTypes;\n    showArea = cache.showArea;\n    opacityVal = cache.opacityVal;\n    fontSizeVal = cache.fontSizeVal;\n    danmakuDurationVal = cache.danmakuDurationVal;\n    strokeWidth = cache.strokeWidth;\n    playRepeat = cache.playRepeat;\n    _playbackSpeed.value = cache.playbackSpeed;\n    enableAutoLongPressSpeed = cache.enableAutoLongPressSpeed;\n    _longPressSpeed.value = cache.longPressSpeed;\n    speedsList = cache.speedsList;\n    // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {\n    //   if (status == PlayerStatus.playing) {\n    //     WakelockPlus.enable();\n    //   } else {\n    //     WakelockPlus.disable();\n    //   }\n    // });\n  }\n\n  // 获取实例 传参\n  factory PlPlayerController({\n    String videoType = 'archive',\n  }) {\n    // 如果实例尚未创建，则创建一个新实例\n    _instance ??= PlPlayerController._internal(videoType);\n    if (videoType != 'none') {\n      _instance!._playerCount.value += 1;\n      _videoType.value = videoType;\n    }\n    return _instance!;\n  }\n\n  // 初始化资源\n  Future<void> setDataSource(\n    DataSource dataSource, {\n    bool autoplay = true,\n    // 默认不循环\n    PlaylistMode looping = PlaylistMode.none,\n    // 初始化播放位置\n    Duration seekTo = Duration.zero,\n    // 初始化播放速度\n    double speed = 1.0,\n    // 硬件加速\n    bool enableHA = false,\n    double? width,\n    double? height,\n    Duration? duration,\n    // 方向\n    String? direction,\n    // 记录历史记录\n    String bvid = '',\n    int cid = 0,\n    // 历史记录开关\n    bool enableHeart = true,\n    // 是否首次加载\n    bool isFirstTime = true,\n    //  是否开启字幕\n    bool enableSubTitle = false,\n  }) async {\n    try {\n      _autoPlay = autoplay;\n      _looping = looping;\n      // 初始化视频倍速\n      // _playbackSpeed.value = speed;\n      // 初始化数据加载状态\n      dataStatus.status.value = DataStatus.loading;\n      // 初始化全屏方向\n      _direction.value = direction ?? 'horizontal';\n      _bvid = bvid;\n      _cid = cid;\n      _enableHeart = enableHeart;\n      _isFirstTime = isFirstTime;\n      _subTitleOpen.value = enableSubTitle;\n      subtitles = [].obs;\n      subtitleContent.value = '';\n      if (_videoPlayerController != null &&\n          _videoPlayerController!.state.playing) {\n        await pause(notify: false);\n      }\n\n      if (_playerCount.value == 0) {\n        return;\n      }\n      // 配置Player 音轨、字幕等等\n      _videoPlayerController = await _createVideoController(\n          dataSource, _looping, enableHA, width, height, seekTo);\n      // 获取视频时长 00:00\n      _duration.value = duration ?? _videoPlayerController!.state.duration;\n      updateDurationSecond();\n      // 数据加载完成\n      dataStatus.status.value = DataStatus.loaded;\n\n      // listen the video player events\n      if (!_listenersInitialized) {\n        startListeners();\n      }\n      await _initializePlayer(duration: _duration.value);\n      bool autoEnterFullcreen =\n          setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false);\n      if (autoEnterFullcreen && _isFirstTime) {\n        await Future.delayed(const Duration(milliseconds: 100));\n        triggerFullScreen();\n      }\n    } catch (err) {\n      dataStatus.status.value = DataStatus.error;\n      print('plPlayer err:  $err');\n    }\n  }\n\n  // 配置播放器\n  Future<Player> _createVideoController(\n    DataSource dataSource,\n    PlaylistMode looping,\n    bool enableHA,\n    double? width,\n    double? height,\n    Duration seekTo,\n  ) async {\n    // 每次配置时先移除监听\n    removeListeners();\n    isBuffering.value = false;\n    buffered.value = Duration.zero;\n    _heartDuration = 0;\n    _position.value = Duration.zero;\n    // 初始化时清空弹幕，防止上次重叠\n    if (danmakuController != null) {\n      danmakuController!.clear();\n    }\n    Player player = _videoPlayerController ??\n        Player(\n          configuration: PlayerConfiguration(\n            // 默认缓存 5M 大小\n            bufferSize:\n                videoType == 'live' ? 32 * 1024 * 1024 : 5 * 1024 * 1024,\n          ),\n        );\n\n    var pp = player.platform as NativePlayer;\n    // 解除倍速限制\n    await pp.setProperty(\"af\", \"scaletempo2=max-speed=8\");\n    //  音量不一致\n    if (Platform.isAndroid) {\n      await pp.setProperty(\"volume-max\", \"100\");\n      String defaultAoOutput =\n          setting.get(SettingBoxKey.defaultAoOutput, defaultValue: '0');\n      await pp.setProperty(\n          \"ao\",\n          aoOutputList\n              .where((e) => e['value'] == defaultAoOutput)\n              .first['title']);\n    }\n\n    await player.setAudioTrack(\n      AudioTrack.auto(),\n    );\n\n    // 音轨\n    if (dataSource.audioSource != '' && dataSource.audioSource != null) {\n      await pp.setProperty(\n        'audio-files',\n        UniversalPlatform.isWindows\n            ? dataSource.audioSource!.replaceAll(';', '\\\\;')\n            : dataSource.audioSource!.replaceAll(':', '\\\\:'),\n      );\n    } else {\n      await pp.setProperty(\n        'audio-files',\n        '',\n      );\n    }\n\n    // 字幕\n    // if (dataSource.subFiles != '' && dataSource.subFiles != null) {\n    //   await pp.setProperty(\n    //     'sub-files',\n    //     UniversalPlatform.isWindows\n    //         ? dataSource.subFiles!.replaceAll(';', '\\\\;')\n    //         : dataSource.subFiles!.replaceAll(':', '\\\\:'),\n    //   );\n    //   await pp.setProperty(\"subs-with-matching-audio\", \"no\");\n    //   await pp.setProperty(\"sub-forced-only\", \"yes\");\n    //   await pp.setProperty(\"blend-subtitles\", \"video\");\n    // }\n\n    _videoController = _videoController ??\n        VideoController(\n          player,\n          configuration: VideoControllerConfiguration(\n            enableHardwareAcceleration: enableHA,\n            androidAttachSurfaceAfterVideoParameters: false,\n          ),\n        );\n\n    player.setPlaylistMode(looping);\n\n    if (dataSource.type == DataSourceType.asset) {\n      final assetUrl = dataSource.videoSource!.startsWith(\"asset://\")\n          ? dataSource.videoSource!\n          : \"asset://${dataSource.videoSource!}\";\n      player.open(\n        Media(assetUrl, httpHeaders: dataSource.httpHeaders),\n        play: false,\n      );\n    }\n    await player.open(\n      Media(dataSource.videoSource!,\n          httpHeaders: dataSource.httpHeaders, start: seekTo),\n      play: false,\n    );\n    // 音轨\n    // player.setAudioTrack(\n    //   AudioTrack.uri(dataSource.audioSource!),\n    // );\n\n    return player;\n  }\n\n  // 开始播放\n  Future _initializePlayer({\n    Duration? duration,\n  }) async {\n    getVideoFit();\n    // if (_looping) {\n    //   await setLooping(_looping);\n    // }\n\n    /// 跳转播放\n    // if (seekTo != Duration.zero) {\n    //   await this.seekTo(seekTo);\n    // }\n\n    /// 自动播放\n    if (_autoPlay) {\n      await play(duration: duration);\n    }\n\n    /// 设置倍速\n    if (videoType == 'live') {\n      await setPlaybackSpeed(1.0);\n    } else {\n      if (_playbackSpeed.value != 1.0) {\n        await setPlaybackSpeed(_playbackSpeed.value);\n      } else {\n        await setPlaybackSpeed(1.0);\n      }\n    }\n  }\n\n  List<StreamSubscription> subscriptions = [];\n  final List<Function(Duration position)> _positionListeners = [];\n  final List<Function(PlayerStatus status)> _statusListeners = [];\n\n  /// 播放事件监听\n  void startListeners() {\n    subscriptions.addAll(\n      [\n        videoPlayerController!.stream.playing.listen((event) {\n          if (event) {\n            playerStatus.status.value = PlayerStatus.playing;\n          } else {\n            playerStatus.status.value = PlayerStatus.paused;\n          }\n          videoPlayerServiceHandler.onStatusChange(\n              playerStatus.status.value, isBuffering.value);\n\n          /// 触发回调事件\n          for (var element in _statusListeners) {\n            element(event ? PlayerStatus.playing : PlayerStatus.paused);\n          }\n          if (videoPlayerController!.state.position.inSeconds != 0) {\n            makeHeartBeat(positionSeconds.value, type: 'status');\n          }\n        }),\n        videoPlayerController!.stream.completed.listen((event) {\n          if (event) {\n            playerStatus.status.value = PlayerStatus.completed;\n\n            /// 触发回调事件\n            for (var element in _statusListeners) {\n              element(PlayerStatus.completed);\n            }\n          } else {\n            // playerStatus.status.value = PlayerStatus.playing;\n          }\n          makeHeartBeat(positionSeconds.value, type: 'status');\n        }),\n        videoPlayerController!.stream.position.listen((event) {\n          _position.value = event;\n          updatePositionSecond();\n          if (!isSliderMoving.value) {\n            _sliderPosition.value = event;\n            updateSliderPositionSecond();\n          }\n          querySubtitleContent(\n              videoPlayerController!.state.position.inSeconds.toDouble());\n\n          /// 触发回调事件\n          for (var element in _positionListeners) {\n            element(event);\n          }\n          makeHeartBeat(event.inSeconds);\n        }),\n        videoPlayerController!.stream.duration.listen((event) {\n          if (event > Duration.zero) {\n            duration.value = event;\n          }\n        }),\n        videoPlayerController!.stream.buffer.listen((event) {\n          _buffered.value = event;\n          updateBufferedSecond();\n        }),\n        videoPlayerController!.stream.buffering.listen((event) {\n          isBuffering.value = event;\n          videoPlayerServiceHandler.onStatusChange(\n              playerStatus.status.value, event);\n        }),\n        // videoPlayerController!.stream.volume.listen((event) {\n        //   if (!mute.value && _volumeBeforeMute != event) {\n        //     _volumeBeforeMute = event / 100;\n        //   }\n        // }),\n        // 媒体通知监听\n        onPlayerStatusChanged.listen((event) {\n          videoPlayerServiceHandler.onStatusChange(event, isBuffering.value);\n        }),\n        onPositionChanged.listen((event) {\n          EasyThrottle.throttle(\n              'mediaServicePositon',\n              const Duration(seconds: 1),\n              () => videoPlayerServiceHandler.onPositionChange(event));\n        }),\n      ],\n    );\n  }\n\n  /// 移除事件监听\n  void removeListeners() {\n    for (final s in subscriptions) {\n      s.cancel();\n    }\n  }\n\n  /// 跳转至指定位置\n  Future<void> seekTo(Duration position, {type = 'seek'}) async {\n    try {\n      if (position < Duration.zero) {\n        position = Duration.zero;\n      }\n      _position.value = position;\n      updatePositionSecond();\n      _heartDuration = position.inSeconds;\n      if (duration.value.inSeconds != 0) {\n        if (type != 'slider') {\n          await _videoPlayerController?.stream.buffer.first;\n        }\n        await _videoPlayerController?.seek(position);\n      } else {\n        _timerForSeek?.cancel();\n        _timerForSeek ??= _startSeekTimer(position);\n      }\n    } catch (err) {\n      print('Error while seeking: $err');\n    }\n  }\n\n  Timer? _startSeekTimer(Duration position) {\n    return Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {\n      if (duration.value.inSeconds != 0) {\n        await _videoPlayerController!.stream.buffer.first;\n        await _videoPlayerController?.seek(position);\n        t.cancel();\n        _timerForSeek = null;\n      }\n    });\n  }\n\n  /// 设置倍速\n  Future<void> setPlaybackSpeed(double speed) async {\n    /// TODO  _duration.value丢失\n    await _videoPlayerController?.setRate(speed);\n    try {\n      DanmakuOption currentOption = danmakuController!.option;\n      defaultDuration ??= currentOption.duration;\n      DanmakuOption updatedOption = currentOption.copyWith(\n          duration: (defaultDuration! / speed) * playbackSpeed);\n      danmakuController!.updateOption(updatedOption);\n    } catch (_) {}\n    // fix 长按倍速后放开不恢复\n    if (!doubleSpeedStatus.value) {\n      _playbackSpeed.value = speed;\n    }\n  }\n\n  // 还原默认速度\n  Future<void> setDefaultSpeed() async {\n    double speed =\n        videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);\n    await _videoPlayerController?.setRate(speed);\n    _playbackSpeed.value = speed;\n  }\n\n  /// 播放视频\n  /// TODO  _duration.value丢失\n  Future<void> play(\n      {bool repeat = false, bool hideControls = true, dynamic duration}) async {\n    // 播放时自动隐藏控制条\n    controls = !hideControls;\n    // repeat为true，将从头播放\n    if (repeat) {\n      await seekTo(Duration.zero);\n    }\n    await _videoPlayerController?.play();\n    playerStatus.status.value = PlayerStatus.playing;\n    await getCurrentVolume();\n    await getCurrentBrightness();\n\n    // screenManager.setOverlays(false);\n\n    /// 临时fix _duration.value丢失\n    if (duration != null) {\n      _duration.value = duration;\n      updateDurationSecond();\n    }\n    audioSessionHandler.setActive(true);\n  }\n\n  /// 暂停播放\n  Future<void> pause({bool notify = true, bool isInterrupt = false}) async {\n    await _videoPlayerController?.pause();\n    playerStatus.status.value = PlayerStatus.paused;\n\n    // 主动暂停时让出音频焦点\n    if (!isInterrupt) {\n      audioSessionHandler.setActive(false);\n    }\n  }\n\n  /// 更改播放状态\n  Future<void> togglePlay() async {\n    feedBack();\n    if (playerStatus.playing) {\n      pause();\n    } else {\n      play();\n    }\n  }\n\n  /// 隐藏控制条\n  void _hideTaskControls() {\n    if (_timer != null) {\n      _timer!.cancel();\n    }\n    _timer = Timer(const Duration(milliseconds: 3000), () {\n      if (!isSliderMoving.value) {\n        controls = false;\n      }\n      _timer = null;\n    });\n  }\n\n  /// 调整播放时间\n  onChangedSlider(double v) {\n    _sliderPosition.value = Duration(seconds: v.floor());\n    updateSliderPositionSecond();\n  }\n\n  void onChangedSliderStart() {\n    _isSliderMoving.value = true;\n  }\n\n  void onUpdatedSliderProgress(Duration value) {\n    _sliderTempPosition.value = value;\n    _sliderPosition.value = value;\n    updateSliderPositionSecond();\n  }\n\n  void onChangedSliderEnd() {\n    feedBack();\n    _isSliderMoving.value = false;\n    _hideTaskControls();\n  }\n\n  /// 音量\n  Future<void> getCurrentVolume() async {\n    // mac try...catch\n    try {\n      _currentVolume.value = (await FlutterVolumeController.getVolume())!;\n    } catch (_) {}\n  }\n\n  Future<void> setVolume(double volumeNew,\n      {bool videoPlayerVolume = false}) async {\n    if (volumeNew < 0.0) {\n      volumeNew = 0.0;\n    } else if (volumeNew > 1.0) {\n      volumeNew = 1.0;\n    }\n    if (volume.value == volumeNew) {\n      return;\n    }\n    volume.value = volumeNew;\n\n    try {\n      FlutterVolumeController.updateShowSystemUI(false);\n      await FlutterVolumeController.setVolume(volumeNew);\n    } catch (err) {\n      print(err);\n    }\n  }\n\n  void volumeUpdated() {\n    showVolumeStatus.value = true;\n    _timerForShowingVolume?.cancel();\n    _timerForShowingVolume = Timer(const Duration(seconds: 1), () {\n      showVolumeStatus.value = false;\n    });\n  }\n\n  /// 亮度\n  Future<void> getCurrentBrightness() async {\n    try {\n      _currentBrightness.value = await ScreenBrightness().current;\n    } catch (e) {\n      throw 'Failed to get current brightness';\n      //return 0;\n    }\n  }\n\n  Future<void> setBrightness(double brightnes) async {\n    try {\n      brightness.value = brightnes;\n      ScreenBrightness().setScreenBrightness(brightnes);\n      // setVideoBrightness();\n    } catch (e) {\n      throw 'Failed to set brightness';\n    }\n  }\n\n  Future<void> resetBrightness() async {\n    try {\n      await ScreenBrightness().resetScreenBrightness();\n    } catch (e) {\n      throw 'Failed to reset brightness';\n    }\n  }\n\n  /// Toggle Change the videofit accordingly\n  void toggleVideoFit() {\n    showDialog(\n      context: Get.context!,\n      builder: (context) {\n        return AlertDialog(\n          title: const Text('画面比例'),\n          content: StatefulBuilder(builder: (context, StateSetter setState) {\n            return Wrap(\n              alignment: WrapAlignment.start,\n              spacing: 8,\n              runSpacing: 2,\n              children: [\n                for (var i in videoFitType) ...[\n                  if (_videoFit.value == i['attr']) ...[\n                    FilledButton(\n                      onPressed: () async {\n                        _videoFit.value = i['attr'];\n                        _videoFitDesc.value = i['desc'];\n                        setVideoFit();\n                        Get.back();\n                      },\n                      child: Text(i['desc']),\n                    ),\n                  ] else ...[\n                    FilledButton.tonal(\n                      onPressed: () async {\n                        _videoFit.value = i['attr'];\n                        _videoFitDesc.value = i['desc'];\n                        setVideoFit();\n                        Get.back();\n                      },\n                      child: Text(i['desc']),\n                    ),\n                  ]\n                ]\n              ],\n            );\n          }),\n        );\n      },\n    );\n  }\n\n  /// 缓存fit\n  Future<void> setVideoFit() async {\n    List attrs = videoFitType.map((e) => e['attr']).toList();\n    int index = attrs.indexOf(_videoFit.value);\n    videoStorage.put(VideoBoxKey.cacheVideoFit, index);\n  }\n\n  /// 读取fit\n  Future<void> getVideoFit() async {\n    int fitValue = videoStorage.get(VideoBoxKey.cacheVideoFit, defaultValue: 0);\n    _videoFit.value = videoFitType[fitValue]['attr'];\n    _videoFitDesc.value = videoFitType[fitValue]['desc'];\n  }\n\n  /// 读取亮度\n  // Future<void> getVideoBrightness() async {\n  //   double brightnessValue =\n  //       videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 0.5);\n  //   setBrightness(brightnessValue);\n  // }\n\n  set controls(bool visible) {\n    _showControls.value = visible;\n    _timer?.cancel();\n    if (visible) {\n      _hideTaskControls();\n    }\n  }\n\n  void hiddenControls(bool val) {\n    showControls.value = val;\n  }\n\n  /// 设置长按倍速状态 live模式下禁用\n  void setDoubleSpeedStatus(bool val) {\n    if (videoType == 'live') {\n      return;\n    }\n    if (controlsLock.value) {\n      return;\n    }\n    _doubleSpeedStatus.value = val;\n    if (val) {\n      setPlaybackSpeed(\n          enableAutoLongPressSpeed ? playbackSpeed * 2 : longPressSpeed);\n    } else {\n      print(playbackSpeed);\n      setPlaybackSpeed(playbackSpeed);\n    }\n  }\n\n  /// 关闭控制栏\n  void onLockControl(bool val) {\n    feedBack();\n    _controlsLock.value = val;\n    showControls.value = !val;\n  }\n\n  void toggleFullScreen(bool val) {\n    _isFullScreen.value = val;\n  }\n\n  // 全屏\n  Future<void> triggerFullScreen({bool status = true}) async {\n    FullScreenMode mode = FullScreenModeCode.fromCode(\n        setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!;\n    await StatusBarControl.setHidden(true, animation: StatusBarAnimation.FADE);\n    if (!isFullScreen.value && status) {\n      /// 按照视频宽高比决定全屏方向\n      toggleFullScreen(true);\n\n      /// 进入全屏\n      await enterFullScreen();\n      if (mode == FullScreenMode.vertical ||\n          (mode == FullScreenMode.auto && direction.value == 'vertical')) {\n        await verticalScreen();\n      } else {\n        await landScape();\n      }\n    } else if (isFullScreen.value && !status) {\n      StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);\n      exitFullScreen();\n      await verticalScreen();\n      toggleFullScreen(false);\n    }\n  }\n\n  void addPositionListener(Function(Duration position) listener) =>\n      _positionListeners.add(listener);\n  void removePositionListener(Function(Duration position) listener) =>\n      _positionListeners.remove(listener);\n  void addStatusLister(Function(PlayerStatus status) listener) =>\n      _statusListeners.add(listener);\n  void removeStatusLister(Function(PlayerStatus status) listener) =>\n      _statusListeners.remove(listener);\n\n  /// 截屏\n  Future screenshot() async {\n    final Uint8List? screenshot =\n        await _videoPlayerController!.screenshot(format: 'image/png');\n    return screenshot;\n  }\n\n  Future<void> videoPlayerClosed() async {\n    _timer?.cancel();\n    _timerForVolume?.cancel();\n    _timerForGettingVolume?.cancel();\n    timerForTrackingMouse?.cancel();\n    _timerForSeek?.cancel();\n    videoFitChangedTimer?.cancel();\n  }\n\n  // 记录播放记录\n  Future makeHeartBeat(int progress, {type = 'playing'}) async {\n    if (!_enableHeart) {\n      return false;\n    }\n    if (videoType == 'live') {\n      return;\n    }\n    // 播放状态变化时，更新\n    if (type == 'status') {\n      await VideoHttp.heartBeat(\n        bvid: _bvid,\n        cid: _cid,\n        progress:\n            playerStatus.status.value == PlayerStatus.completed ? -1 : progress,\n      );\n    } else\n    // 正常播放时，间隔5秒更新一次\n    if (progress - _heartDuration >= 5) {\n      _heartDuration = progress;\n      await VideoHttp.heartBeat(\n        bvid: _bvid,\n        cid: _cid,\n        progress: progress,\n      );\n    }\n  }\n\n  /// 字幕\n  void toggleSubtitle(int code) {\n    _subTitleOpen.value = code != -1;\n    _subTitleCode.value = code;\n  }\n\n  void querySubtitleContent(double progress) {\n    if (subTitleCode.value == -1) {\n      subtitleContent.value = '';\n      return;\n    }\n    if (subtitles.isEmpty) {\n      return;\n    }\n    final SubTitlteItemModel? subtitle = subtitles.firstWhereOrNull(\n      (element) => element.id == subTitleCode.value,\n    );\n    if (subtitle != null && subtitle.body!.isNotEmpty) {\n      for (var content in subtitle.body!) {\n        if (progress >= content['from']! && progress <= content['to']!) {\n          subtitleContent.value = content['content']!;\n          return;\n        }\n      }\n    }\n  }\n\n  setPlayRepeat(PlayRepeat type) {\n    playRepeat = type;\n    videoStorage.put(VideoBoxKey.playRepeat, type.value);\n  }\n\n  /// 缓存本次弹幕选项\n  cacheDanmakuOption() {\n    localCache.put(LocalCacheKey.danmakuBlockType, blockTypes);\n    localCache.put(LocalCacheKey.danmakuShowArea, showArea);\n    localCache.put(LocalCacheKey.danmakuOpacity, opacityVal);\n    localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);\n    localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal);\n    localCache.put(LocalCacheKey.strokeWidth, strokeWidth);\n  }\n\n  Future<void> dispose({String type = 'single'}) async {\n    // 每次减1，最后销毁\n    if (type == 'single' && playerCount.value > 1) {\n      _playerCount.value -= 1;\n      _heartDuration = 0;\n      pause();\n      return;\n    }\n    _playerCount.value = 0;\n    try {\n      _timer?.cancel();\n      _timerForVolume?.cancel();\n      _timerForGettingVolume?.cancel();\n      timerForTrackingMouse?.cancel();\n      _timerForSeek?.cancel();\n      videoFitChangedTimer?.cancel();\n      // _position.close();\n      _playerEventSubs?.cancel();\n      // _sliderPosition.close();\n      // _sliderTempPosition.close();\n      // _isSliderMoving.close();\n      // _duration.close();\n      // _buffered.close();\n      // _showControls.close();\n      // _controlsLock.close();\n      // playerStatus.status.close();\n      // dataStatus.status.close();\n\n      /// 缓存本次弹幕选项\n      cacheDanmakuOption();\n      if (_videoPlayerController != null) {\n        var pp = _videoPlayerController!.platform as NativePlayer;\n        await pp.setProperty('audio-files', '');\n        removeListeners();\n        await _videoPlayerController?.dispose();\n        _videoPlayerController = null;\n      }\n      _instance = null;\n      // 关闭所有视频页面恢复亮度\n      resetBrightness();\n      videoPlayerServiceHandler.clear();\n    } catch (err) {\n      print(err);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/index.dart",
    "content": "library pl_player;\n\nexport './controller.dart';\nexport './view.dart';\nexport './models/data_source.dart';\nexport './models/play_status.dart';\nexport './models/data_status.dart';\nexport './widgets/common_btn.dart';\nexport './models/play_speed.dart';\nexport './models/fullscreen_mode.dart';\nexport './models/bottom_progress_behavior.dart';\nexport './widgets/app_bar_ani.dart';\nexport './utils/fullscreen.dart';\nexport './utils.dart';\n"
  },
  {
    "path": "lib/plugin/pl_player/models/bottom_control_type.dart",
    "content": "enum BottomControlType {\n  pre,\n  playOrPause,\n  next,\n  time,\n  space,\n  episode,\n  fit,\n  speed,\n  fullscreen,\n  custom,\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/models/bottom_progress_behavior.dart",
    "content": "// ignore: camel_case_types\nenum BtmProgresBehavior {\n  alwaysShow,\n  alwaysHide,\n  onlyShowFullScreen,\n  onlyHideFullScreen,\n}\n\nextension BtmProgresBehaviorDesc on BtmProgresBehavior {\n  String get description => ['始终展示', '始终隐藏', '仅全屏时展示', '仅全屏时隐藏'][index];\n}\n\nextension BtmProgresBehaviorCode on BtmProgresBehavior {\n  static final List<int> _codeList = [0, 1, 2, 3];\n  int get code => _codeList[index];\n\n  static BtmProgresBehavior? fromCode(int code) {\n    final index = _codeList.indexOf(code);\n    if (index != -1) {\n      return BtmProgresBehavior.values[index];\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/models/data_source.dart",
    "content": "import 'dart:io';\n\n/// The way in which the video was originally loaded.\n///\n/// This has nothing to do with the video's file type. It's just the place\n/// from which the video is fetched from.\nenum DataSourceType {\n  /// The video was included in the app's asset files.\n  asset,\n\n  /// The video was downloaded from the internet.\n  network,\n\n  /// The video was loaded off of the local filesystem.\n  file,\n\n  /// The video is available via contentUri. Android only.\n  contentUri,\n}\n\nclass DataSource {\n  File? file;\n  String? videoSource;\n  String? audioSource;\n  String? subFiles;\n  DataSourceType type;\n  Map<String, String>? httpHeaders; // for headers\n  DataSource({\n    this.file,\n    this.videoSource,\n    this.audioSource,\n    this.subFiles,\n    required this.type,\n    this.httpHeaders,\n  }) : assert((type == DataSourceType.file && file != null) ||\n            videoSource != null);\n\n  DataSource copyWith({\n    File? file,\n    String? videoSource,\n    String? audioSource,\n    String? subFiles,\n    DataSourceType? type,\n    Map<String, String>? httpHeaders,\n  }) {\n    return DataSource(\n      file: file ?? this.file,\n      videoSource: videoSource ?? this.videoSource,\n      audioSource: audioSource ?? this.audioSource,\n      subFiles: subFiles ?? this.subFiles,\n      type: type ?? this.type,\n      httpHeaders: httpHeaders ?? this.httpHeaders,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/models/data_status.dart",
    "content": "import 'package:get/get.dart';\n\nenum DataStatus { none, loading, loaded, error }\n\nclass PlPlayerDataStatus {\n  Rx<DataStatus> status = Rx(DataStatus.none);\n\n  bool get none => status.value == DataStatus.none;\n  bool get loading => status.value == DataStatus.loading;\n  bool get loaded => status.value == DataStatus.loaded;\n  bool get error => status.value == DataStatus.error;\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/models/duration.dart",
    "content": "extension DurationExtension on Duration {\n  /// Returns clamp of [Duration] between [min] and [max].\n  Duration clamp(Duration min, Duration max) {\n    if (this < min) return min;\n    if (this > max) return max;\n    return this;\n  }\n\n  /// Returns a [String] representation of [Duration].\n  String label({Duration? reference}) {\n    reference ??= this;\n    if (reference > const Duration(days: 1)) {\n      final days = inDays.toString().padLeft(3, '0');\n      final hours = (inHours - (inDays * 24)).toString().padLeft(2, '0');\n      final minutes = (inMinutes - (inHours * 60)).toString().padLeft(2, '0');\n      final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');\n      return '$days:$hours:$minutes:$seconds';\n    } else if (reference > const Duration(hours: 1)) {\n      final hours = inHours.toString().padLeft(2, '0');\n      final minutes = (inMinutes - (inHours * 60)).toString().padLeft(2, '0');\n      final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');\n      return '$hours:$minutes:$seconds';\n    } else {\n      final minutes = inMinutes.toString().padLeft(2, '0');\n      final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');\n      return '$minutes:$seconds';\n    }\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/models/fullscreen_mode.dart",
    "content": "// 全屏模式\nenum FullScreenMode {\n  // 根据内容自适应\n  auto,\n  // 始终竖屏\n  vertical,\n  // 始终横屏\n  horizontal\n}\n\nextension FullScreenModeDesc on FullScreenMode {\n  String get description => ['自适应', '始终竖屏', '始终横屏'][index];\n}\n\nextension FullScreenModeCode on FullScreenMode {\n  static final List<int> _codeList = [0, 1, 2];\n  int get code => _codeList[index];\n\n  static FullScreenMode? fromCode(int code) {\n    final index = _codeList.indexOf(code);\n    if (index != -1) {\n      return FullScreenMode.values[index];\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/models/play_repeat.dart",
    "content": "enum PlayRepeat {\n  pause,\n  listOrder,\n  singleCycle,\n  listCycle,\n}\n\nextension PlayRepeatExtension on PlayRepeat {\n  static final List<String> _descList = <String>[\n    '播完暂停',\n    '顺序播放',\n    '单个循环',\n    '列表循环',\n  ];\n  String get description => _descList[index];\n\n  static final List<double> _valueList = [\n    1,\n    2,\n    3,\n    4,\n  ];\n  double get value => _valueList[index];\n  double get defaultValue => _valueList[1];\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/models/play_speed.dart",
    "content": "List<double> generatePlaySpeedList() {\n  List<double> playSpeed = [];\n  double startSpeed = 0.25;\n  double endSpeed = 2.0;\n  double increment = 0.25;\n\n  for (double speed = startSpeed; speed <= endSpeed; speed += increment) {\n    playSpeed.add(speed);\n  }\n\n  return playSpeed;\n}\n\n// 导出 playSpeed 列表\nList<double> playSpeed = generatePlaySpeedList();\n"
  },
  {
    "path": "lib/plugin/pl_player/models/play_status.dart",
    "content": "import 'package:get/get.dart';\n\nenum PlayerStatus { completed, playing, paused }\n\nclass PlPlayerStatus {\n  Rx<PlayerStatus> status = Rx(PlayerStatus.paused);\n\n  bool get playing {\n    return status.value == PlayerStatus.playing;\n  }\n\n  bool get paused {\n    return status.value == PlayerStatus.paused;\n  }\n\n  bool get completed {\n    return status.value == PlayerStatus.completed;\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/utils/fullscreen.dart",
    "content": "import 'dart:io';\n\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:auto_orientation/auto_orientation.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\n\n//横屏\nFuture<void> landScape() async {\n  dynamic document;\n  try {\n    if (kIsWeb) {\n      await document.documentElement?.requestFullscreen();\n    } else if (Platform.isAndroid || Platform.isIOS) {\n      // await SystemChrome.setEnabledSystemUIMode(\n      //   SystemUiMode.immersiveSticky,\n      //   overlays: [],\n      // );\n      // await SystemChrome.setPreferredOrientations(\n      //   [\n      //     DeviceOrientation.landscapeLeft,\n      //     DeviceOrientation.landscapeRight,\n      //   ],\n      // );\n      await AutoOrientation.landscapeAutoMode(forceSensor: true);\n    } else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {\n      await const MethodChannel('com.alexmercerind/media_kit_video')\n          .invokeMethod(\n        'Utils.EnterNativeFullscreen',\n      );\n    }\n  } catch (exception, stacktrace) {\n    debugPrint(exception.toString());\n    debugPrint(stacktrace.toString());\n  }\n}\n\n//竖屏\nFuture<void> verticalScreen() async {\n  await SystemChrome.setPreferredOrientations([\n    DeviceOrientation.portraitUp,\n  ]);\n}\n\nFuture<void> enterFullScreen() async {\n  await SystemChrome.setEnabledSystemUIMode(\n    SystemUiMode.immersiveSticky,\n  );\n}\n\n//退出全屏显示\nFuture<void> exitFullScreen() async {\n  dynamic document;\n  late SystemUiMode mode = SystemUiMode.edgeToEdge;\n  try {\n    if (kIsWeb) {\n      document.exitFullscreen();\n    } else if (Platform.isAndroid || Platform.isIOS) {\n      if (Platform.isAndroid &&\n          (await DeviceInfoPlugin().androidInfo).version.sdkInt < 29) {\n        mode = SystemUiMode.manual;\n      }\n      await SystemChrome.setEnabledSystemUIMode(\n        mode,\n        overlays: SystemUiOverlay.values,\n      );\n      await SystemChrome.setPreferredOrientations([]);\n    } else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {\n      await const MethodChannel('com.alexmercerind/media_kit_video')\n          .invokeMethod(\n        'Utils.ExitNativeFullscreen',\n      );\n    }\n  } catch (exception, stacktrace) {\n    debugPrint(exception.toString());\n    debugPrint(stacktrace.toString());\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/utils.dart",
    "content": "String printDuration(Duration? duration) {\n  if (duration == null) return \"--:--\";\n\n  /*String twoDigits(int n) {\n    if (n >= 10||n < 0) return \"$n\";\n    return \"0$n\";\n  }*/\n  String twoDigits(int n) => n.toString().padLeft(2, \"0\");\n\n  String twoDigitMinutes = twoDigits(duration.inMinutes).replaceAll(\"-\", \"\");\n  String twoDigitSeconds =\n      twoDigits(duration.inSeconds.remainder(60)).replaceAll(\"-\", \"\");\n  //customDebugPrint(duration.inSeconds.remainder(60));\n  return \"$twoDigitMinutes:$twoDigitSeconds\";\n}\n\nString printDurationWithHours(Duration? duration) {\n  if (duration == null) return \"--:--:--\";\n\n  String twoDigits(int n) {\n    if (n >= 10) return \"$n\";\n    return \"0$n\";\n  }\n\n  String twoDigitHours = twoDigits(duration.inHours);\n  String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));\n  String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));\n  return \"$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds\";\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/view.dart",
    "content": "import 'dart:async';\n\nimport 'package:audio_video_progress_bar/audio_video_progress_bar.dart';\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_volume_controller/flutter_volume_controller.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:lottie/lottie.dart';\nimport 'package:media_kit/media_kit.dart';\nimport 'package:media_kit_video/media_kit_video.dart';\nimport 'package:pilipala/models/common/gesture_mode.dart';\nimport 'package:pilipala/plugin/pl_player/controller.dart';\nimport 'package:pilipala/plugin/pl_player/models/duration.dart';\nimport 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart';\nimport 'package:pilipala/plugin/pl_player/utils.dart';\nimport 'package:pilipala/utils/feed_back.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:screen_brightness/screen_brightness.dart';\n\nimport '../../utils/global_data_cache.dart';\nimport 'models/bottom_control_type.dart';\nimport 'models/bottom_progress_behavior.dart';\nimport 'widgets/app_bar_ani.dart';\nimport 'widgets/backward_seek.dart';\nimport 'widgets/bottom_control.dart';\nimport 'widgets/common_btn.dart';\nimport 'widgets/control_bar.dart';\nimport 'widgets/forward_seek.dart';\nimport 'widgets/play_pause_btn.dart';\n\nclass PLVideoPlayer extends StatefulWidget {\n  const PLVideoPlayer({\n    required this.controller,\n    this.headerControl,\n    this.bottomControl,\n    this.danmuWidget,\n    this.bottomList,\n    this.customWidget,\n    this.customWidgets,\n    this.showEposideCb,\n    this.fullScreenCb,\n    this.alignment = Alignment.center,\n    super.key,\n  });\n\n  final PlPlayerController controller;\n  final PreferredSizeWidget? headerControl;\n  final PreferredSizeWidget? bottomControl;\n  final Widget? danmuWidget;\n  final List<BottomControlType>? bottomList;\n  // List<Widget> or Widget\n\n  final Widget? customWidget;\n  final List<Widget>? customWidgets;\n  final Function? showEposideCb;\n  final Function? fullScreenCb;\n  final Alignment? alignment;\n\n  @override\n  State<PLVideoPlayer> createState() => _PLVideoPlayerState();\n}\n\nclass _PLVideoPlayerState extends State<PLVideoPlayer>\n    with TickerProviderStateMixin {\n  late AnimationController animationController;\n  late VideoController videoController;\n\n  final RxBool _mountSeekBackwardButton = false.obs;\n  final RxBool _mountSeekForwardButton = false.obs;\n  final RxBool _hideSeekBackwardButton = false.obs;\n  final RxBool _hideSeekForwardButton = false.obs;\n\n  final RxDouble _brightnessValue = 0.0.obs;\n  final RxBool _brightnessIndicator = false.obs;\n  Timer? _brightnessTimer;\n\n  final RxDouble _volumeValue = 0.0.obs;\n  final RxBool _volumeIndicator = false.obs;\n  Timer? _volumeTimer;\n\n  final RxDouble _distance = 0.0.obs;\n  final RxBool _volumeInterceptEventStream = false.obs;\n\n  Box setting = GStrorage.setting;\n  late FullScreenMode mode;\n  late int defaultBtmProgressBehavior;\n  late bool enableQuickDouble;\n  late bool enableBackgroundPlay;\n  late double screenWidth;\n  final FullScreenGestureMode fullScreenGestureMode =\n      GlobalDataCache().fullScreenGestureMode;\n\n  // 用于记录上一次全屏切换手势触发时间，避免误触\n  DateTime? lastFullScreenToggleTime;\n\n  void onDoubleTapSeekBackward() {\n    _mountSeekBackwardButton.value = true;\n  }\n\n  void onDoubleTapSeekForward() {\n    _mountSeekForwardButton.value = true;\n  }\n\n  // 双击播放、暂停\n  void onDoubleTapCenter() {\n    final PlPlayerController _ = widget.controller;\n    _.videoPlayerController!.playOrPause();\n  }\n\n  void doubleTapFuc(String type) {\n    if (!enableQuickDouble) {\n      onDoubleTapCenter();\n      return;\n    }\n    switch (type) {\n      case 'left':\n        // 双击左边区域 👈\n        onDoubleTapSeekBackward();\n        break;\n      case 'center':\n        onDoubleTapCenter();\n        break;\n      case 'right':\n        // 双击右边区域 👈\n        onDoubleTapSeekForward();\n        break;\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    screenWidth = Get.size.width;\n    animationController = AnimationController(\n      vsync: this,\n      duration: GlobalDataCache().enablePlayerControlAnimation\n          ? const Duration(milliseconds: 150)\n          : const Duration(milliseconds: 10),\n    );\n    videoController = widget.controller.videoController!;\n    widget.controller.headerControl = widget.headerControl;\n    widget.controller.bottomControl = widget.bottomControl;\n    widget.controller.danmuWidget = widget.danmuWidget;\n    defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior,\n        defaultValue: BtmProgresBehavior.values.first.code);\n    enableQuickDouble =\n        setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true);\n    enableBackgroundPlay =\n        setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);\n    Future.microtask(() async {\n      try {\n        FlutterVolumeController.updateShowSystemUI(true);\n        _volumeValue.value = (await FlutterVolumeController.getVolume())!;\n        FlutterVolumeController.addListener((double value) {\n          if (mounted && !_volumeInterceptEventStream.value) {\n            _volumeValue.value = value;\n          }\n        });\n      } catch (_) {}\n    });\n\n    Future.microtask(() async {\n      try {\n        _brightnessValue.value = await ScreenBrightness().current;\n        ScreenBrightness().onCurrentBrightnessChanged.listen((double value) {\n          if (mounted) {\n            _brightnessValue.value = value;\n          }\n        });\n      } catch (_) {}\n    });\n  }\n\n  Future<void> setVolume(double value) async {\n    try {\n      FlutterVolumeController.updateShowSystemUI(false);\n      await FlutterVolumeController.setVolume(value);\n    } catch (_) {}\n    _volumeValue.value = value;\n    _volumeIndicator.value = true;\n    _volumeInterceptEventStream.value = true;\n    _volumeTimer?.cancel();\n    _volumeTimer = Timer(const Duration(milliseconds: 200), () {\n      if (mounted) {\n        _volumeIndicator.value = false;\n        _volumeInterceptEventStream.value = false;\n      }\n    });\n  }\n\n  Future<void> setBrightness(double value) async {\n    try {\n      await ScreenBrightness().setScreenBrightness(value);\n    } catch (_) {}\n    _brightnessIndicator.value = true;\n    _brightnessTimer?.cancel();\n    _brightnessTimer = Timer(const Duration(milliseconds: 200), () {\n      if (mounted) {\n        _brightnessIndicator.value = false;\n      }\n    });\n    widget.controller.brightness.value = value;\n  }\n\n  @override\n  void dispose() {\n    animationController.dispose();\n    FlutterVolumeController.removeListener();\n    super.dispose();\n  }\n\n  // 动态构建底部控制条\n  List<Widget> buildBottomControl() {\n    const TextStyle textStyle = TextStyle(\n      color: Colors.white,\n      fontSize: 12,\n    );\n    final PlPlayerController _ = widget.controller;\n    Map<BottomControlType, Widget> videoProgressWidgets = {\n      /// 上一集\n      BottomControlType.pre: ComBtn(\n        icon: const Icon(\n          Icons.skip_previous_rounded,\n          size: 21,\n          color: Colors.white,\n        ),\n        fuc: () {},\n      ),\n\n      /// 播放暂停\n      BottomControlType.playOrPause: PlayOrPauseButton(\n        controller: _,\n      ),\n\n      /// 下一集\n      BottomControlType.next: ComBtn(\n        icon: const Icon(\n          Icons.skip_next_rounded,\n          size: 21,\n          color: Colors.white,\n        ),\n        fuc: () {},\n      ),\n\n      /// 时间进度\n      BottomControlType.time: Row(\n        children: [\n          const SizedBox(width: 8),\n          Obx(() {\n            return Text(\n              _.durationSeconds.value >= 3600\n                  ? printDurationWithHours(\n                      Duration(seconds: _.positionSeconds.value))\n                  : printDuration(Duration(seconds: _.positionSeconds.value)),\n              style: textStyle,\n            );\n          }),\n          const SizedBox(width: 2),\n          const Text('/', style: textStyle),\n          const SizedBox(width: 2),\n          Obx(\n            () => Text(\n              _.durationSeconds.value >= 3600\n                  ? printDurationWithHours(\n                      Duration(seconds: _.durationSeconds.value))\n                  : printDuration(Duration(seconds: _.durationSeconds.value)),\n              style: textStyle,\n            ),\n          ),\n        ],\n      ),\n\n      /// 空白占位\n      BottomControlType.space: const Spacer(),\n\n      /// 选集\n      BottomControlType.episode: SizedBox(\n        height: 30,\n        width: 30,\n        child: TextButton(\n          onPressed: () {\n            widget.showEposideCb?.call();\n          },\n          style: ButtonStyle(\n            padding: MaterialStateProperty.all(EdgeInsets.zero),\n          ),\n          child: const Text(\n            '选集',\n            style: TextStyle(color: Colors.white, fontSize: 13),\n          ),\n        ),\n      ),\n\n      /// 画面比例\n      BottomControlType.fit: SizedBox(\n        height: 30,\n        child: TextButton(\n          onPressed: () => _.toggleVideoFit(),\n          style: ButtonStyle(\n            padding: MaterialStateProperty.all(EdgeInsets.zero),\n          ),\n          child: Obx(\n            () => Text(\n              _.videoFitDEsc.value,\n              style: const TextStyle(color: Colors.white, fontSize: 13),\n            ),\n          ),\n        ),\n      ),\n\n      /// 播放速度\n      BottomControlType.speed: SizedBox(\n        width: 45,\n        height: 34,\n        child: TextButton(\n          style: ButtonStyle(\n            padding: MaterialStateProperty.all(EdgeInsets.zero),\n          ),\n          onPressed: () {},\n          child: Obx(\n            () => Text(\n              '${_.playbackSpeed.toString()}X',\n              style: textStyle,\n            ),\n          ),\n        ),\n      ),\n\n      /// 字幕\n      /// 全屏\n      BottomControlType.fullscreen: ComBtn(\n        icon: Obx(\n          () => Icon(\n            _.isFullScreen.value\n                ? FontAwesomeIcons.compress\n                : FontAwesomeIcons.expand,\n            size: 15,\n            color: Colors.white,\n          ),\n        ),\n        fuc: () {\n          _.triggerFullScreen(status: !_.isFullScreen.value);\n          widget.fullScreenCb?.call(!_.isFullScreen.value);\n        },\n      ),\n    };\n    final List<Widget> list = [];\n    List<BottomControlType> userSpecifyItem = widget.bottomList ??\n        [\n          BottomControlType.playOrPause,\n          BottomControlType.time,\n          BottomControlType.space,\n          BottomControlType.fit,\n          BottomControlType.fullscreen,\n        ];\n    for (var i = 0; i < userSpecifyItem.length; i++) {\n      if (userSpecifyItem[i] == BottomControlType.custom) {\n        if (widget.customWidget != null && widget.customWidget is Widget) {\n          list.add(widget.customWidget!);\n        }\n        if (widget.customWidgets != null && widget.customWidgets!.isNotEmpty) {\n          list.addAll(widget.customWidgets!);\n        }\n      } else {\n        list.add(videoProgressWidgets[userSpecifyItem[i]]!);\n      }\n    }\n    return list;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final PlPlayerController _ = widget.controller;\n    final Color colorTheme = Theme.of(context).colorScheme.primary;\n    const TextStyle subTitleStyle = TextStyle(\n      height: 1.5,\n      fontSize: 40.0,\n      letterSpacing: 0.0,\n      wordSpacing: 0.0,\n      color: Color(0xffffffff),\n      fontWeight: FontWeight.normal,\n      backgroundColor: Color(0xaa000000),\n    );\n    const TextStyle textStyle = TextStyle(\n      color: Colors.white,\n      fontSize: 12,\n    );\n    return Stack(\n      fit: StackFit.passthrough,\n      children: <Widget>[\n        Obx(\n          () => Video(\n            key: ValueKey(_.videoFit.value),\n            controller: videoController,\n            controls: NoVideoControls,\n            alignment: widget.alignment!,\n            pauseUponEnteringBackgroundMode: !enableBackgroundPlay,\n            resumeUponEnteringForegroundMode: true,\n            subtitleViewConfiguration: const SubtitleViewConfiguration(\n              style: subTitleStyle,\n              padding: EdgeInsets.all(24.0),\n            ),\n            fit: _.videoFit.value,\n          ),\n        ),\n\n        /// 长按倍速 toast\n        Obx(\n          () => Align(\n            alignment: Alignment.topCenter,\n            child: FractionalTranslation(\n              translation: const Offset(0.0, 0.3), // 上下偏移量（负数向上偏移）\n              child: AnimatedOpacity(\n                curve: Curves.easeInOut,\n                opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0,\n                duration: const Duration(milliseconds: 150),\n                child: Container(\n                    alignment: Alignment.center,\n                    decoration: BoxDecoration(\n                      color: const Color(0x88000000),\n                      borderRadius: BorderRadius.circular(16.0),\n                    ),\n                    height: 32.0,\n                    width: 70.0,\n                    child: const Center(\n                      child: Text(\n                        '倍速中',\n                        style: TextStyle(color: Colors.white, fontSize: 13),\n                      ),\n                    )),\n              ),\n            ),\n          ),\n        ),\n\n        /// 时间进度 toast\n        Obx(\n          () => Align(\n            alignment: Alignment.topCenter,\n            child: FractionalTranslation(\n              translation: const Offset(0.0, 1.0), // 上下偏移量（负数向上偏移）\n              child: AnimatedOpacity(\n                curve: Curves.easeInOut,\n                opacity: _.isSliderMoving.value ? 1.0 : 0.0,\n                duration: const Duration(milliseconds: 150),\n                child: IntrinsicWidth(\n                  child: Container(\n                    alignment: Alignment.center,\n                    decoration: BoxDecoration(\n                      color: const Color(0x88000000),\n                      borderRadius: BorderRadius.circular(64.0),\n                    ),\n                    height: 34.0,\n                    padding: const EdgeInsets.only(left: 10, right: 10),\n                    child: Row(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Obx(() {\n                          return Text(\n                            _.sliderTempPosition.value.inMinutes >= 60\n                                ? printDurationWithHours(\n                                    _.sliderTempPosition.value)\n                                : printDuration(_.sliderTempPosition.value),\n                            style: textStyle,\n                          );\n                        }),\n                        const SizedBox(width: 2),\n                        const Text('/', style: textStyle),\n                        const SizedBox(width: 2),\n                        Obx(\n                          () => Text(\n                            _.duration.value.inMinutes >= 60\n                                ? printDurationWithHours(_.duration.value)\n                                : printDuration(_.duration.value),\n                            style: textStyle,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n\n        /// 音量🔊 控制条展示\n        Obx(\n          () => ControlBar(\n            visible: _volumeIndicator.value,\n            icon: _volumeValue.value < 1.0 / 3.0\n                ? Icons.volume_mute\n                : _volumeValue.value < 2.0 / 3.0\n                    ? Icons.volume_down\n                    : Icons.volume_up,\n            value: _volumeValue.value,\n          ),\n        ),\n\n        /// 亮度🌞 控制条展示\n        Obx(\n          () => ControlBar(\n            visible: _brightnessIndicator.value,\n            icon: _brightnessValue.value < 1.0 / 3.0\n                ? Icons.brightness_low\n                : _brightnessValue.value < 2.0 / 3.0\n                    ? Icons.brightness_medium\n                    : Icons.brightness_high,\n            value: _brightnessValue.value,\n          ),\n        ),\n\n        // Obx(() {\n        //   if (_.buffered.value == Duration.zero) {\n        //     return Positioned.fill(\n        //       child: Container(\n        //         color: Colors.black,\n        //         child: Center(\n        //           child: Image.asset(\n        //             'assets/images/loading.gif',\n        //             height: 25,\n        //           ),\n        //         ),\n        //       ),\n        //     );\n        //   } else {\n        //     return Container();\n        //   }\n        // }),\n\n        /// 弹幕面板\n        if (widget.danmuWidget != null)\n          Positioned.fill(top: 4, child: widget.danmuWidget!),\n\n        /// 开启且有字幕时展示\n        Stack(\n          children: [\n            Positioned(\n              left: 0,\n              right: 0,\n              bottom: 30,\n              child: Align(\n                alignment: Alignment.center,\n                child: Obx(\n                  () => Visibility(\n                      visible: widget.controller.subTitleCode.value != -1,\n                      child: Container(\n                        decoration: BoxDecoration(\n                          borderRadius: BorderRadius.circular(4),\n                          color: widget.controller.subtitleContent.value != ''\n                              ? Colors.black.withOpacity(0.6)\n                              : Colors.transparent,\n                        ),\n                        padding: widget.controller.subTitleCode.value != -1\n                            ? const EdgeInsets.symmetric(\n                                horizontal: 10,\n                                vertical: 4,\n                              )\n                            : EdgeInsets.zero,\n                        child: Text(\n                          widget.controller.subtitleContent.value,\n                          style: const TextStyle(\n                            color: Colors.white,\n                            fontSize: 12,\n                          ),\n                        ),\n                      )),\n                ),\n              ),\n            ),\n          ],\n        ),\n\n        /// 手势\n        Positioned.fill(\n          left: 16,\n          top: 25,\n          right: 15,\n          bottom: 15,\n          child: GestureDetector(\n            onTap: () {\n              _.controls = !_.showControls.value;\n            },\n            onDoubleTapDown: (TapDownDetails details) {\n              // live模式下禁用 锁定时🔒禁用\n              if (_.videoType == 'live' || _.controlsLock.value) {\n                return;\n              }\n              final double totalWidth = MediaQuery.sizeOf(context).width;\n              final double tapPosition = details.localPosition.dx;\n              final double sectionWidth = totalWidth / 3;\n              String type = 'left';\n              if (tapPosition < sectionWidth) {\n                type = 'left';\n              } else if (tapPosition < sectionWidth * 2) {\n                type = 'center';\n              } else {\n                type = 'right';\n              }\n              doubleTapFuc(type);\n            },\n            onLongPressStart: (LongPressStartDetails detail) {\n              feedBack();\n              _.setDoubleSpeedStatus(true);\n            },\n            onLongPressEnd: (LongPressEndDetails details) {\n              _.setDoubleSpeedStatus(false);\n            },\n\n            /// 水平位置 快进 live模式下禁用\n            onHorizontalDragUpdate: (DragUpdateDetails details) {\n              // live模式下禁用 锁定时🔒禁用\n              if (_.videoType == 'live' || _.controlsLock.value) {\n                return;\n              }\n              // final double tapPosition = details.localPosition.dx;\n              final int curSliderPosition =\n                  _.sliderPosition.value.inMilliseconds;\n              final double scale = 90000 / MediaQuery.sizeOf(context).width;\n              final Duration pos = Duration(\n                  milliseconds:\n                      curSliderPosition + (details.delta.dx * scale).round());\n              final Duration result =\n                  pos.clamp(Duration.zero, _.duration.value);\n              _.onUpdatedSliderProgress(result);\n              _.onChangedSliderStart();\n            },\n            onHorizontalDragEnd: (DragEndDetails details) {\n              if (_.videoType == 'live' || _.controlsLock.value) {\n                return;\n              }\n              _.onChangedSliderEnd();\n              _.seekTo(_.sliderPosition.value, type: 'slider');\n            },\n            // 垂直方向 音量/亮度调节\n            onVerticalDragUpdate: (DragUpdateDetails details) async {\n              final double totalWidth = MediaQuery.sizeOf(context).width;\n              final double tapPosition = details.localPosition.dx;\n              final double sectionWidth = totalWidth / 3;\n              final double delta = details.delta.dy;\n\n              /// 锁定时禁用\n              if (_.controlsLock.value) {\n                return;\n              }\n              if (lastFullScreenToggleTime != null &&\n                  DateTime.now().difference(lastFullScreenToggleTime!) <\n                      const Duration(milliseconds: 500)) {\n                return;\n              }\n              if (tapPosition < sectionWidth) {\n                // 左边区域 👈\n                final double level = (_.isFullScreen.value\n                        ? Get.size.height\n                        : screenWidth * 9 / 16) *\n                    3;\n                final double brightness =\n                    _brightnessValue.value - delta / level;\n                final double result = brightness.clamp(0.0, 1.0);\n                setBrightness(result);\n              } else if (tapPosition < sectionWidth * 2) {\n                // 全屏\n                final double dy = details.delta.dy;\n                const double threshold = 7.0; // 滑动阈值\n                final bool flag =\n                    fullScreenGestureMode != FullScreenGestureMode.values.last;\n                if (dy > _distance.value &&\n                    dy > threshold &&\n                    !_.controlsLock.value) {\n                  if (_.isFullScreen.value ^ flag) {\n                    lastFullScreenToggleTime = DateTime.now();\n                    // 下滑退出全屏\n                    await widget.controller.triggerFullScreen(status: flag);\n                  }\n                  _distance.value = 0.0;\n                } else if (dy < _distance.value &&\n                    dy < -threshold &&\n                    !_.controlsLock.value) {\n                  if (!_.isFullScreen.value ^ flag) {\n                    lastFullScreenToggleTime = DateTime.now();\n                    // 上滑进入全屏\n                    await widget.controller.triggerFullScreen(status: !flag);\n                  }\n                  _distance.value = 0.0;\n                }\n                _distance.value = dy;\n              } else {\n                // 右边区域 👈\n                EasyThrottle.throttle(\n                    'setVolume', const Duration(milliseconds: 20), () {\n                  final double level = (_.isFullScreen.value\n                      ? Get.size.height\n                      : screenWidth * 9 / 16);\n                  final double volume = _volumeValue.value -\n                      double.parse(delta.toStringAsFixed(1)) / level;\n                  final double result = volume.clamp(0.0, 1.0);\n                  setVolume(result);\n                });\n              }\n            },\n            onVerticalDragEnd: (DragEndDetails details) {},\n          ),\n        ),\n\n        // 头部、底部控制条\n        Obx(\n          () => Column(\n            children: [\n              if (widget.headerControl != null || _.headerControl != null)\n                ClipRect(\n                  child: AppBarAni(\n                    controller: animationController,\n                    visible: !_.controlsLock.value && _.showControls.value,\n                    position: 'top',\n                    child: widget.headerControl ?? _.headerControl!,\n                  ),\n                ),\n              const Spacer(),\n              ClipRect(\n                child: AppBarAni(\n                  controller: animationController,\n                  visible: !_.controlsLock.value && _.showControls.value,\n                  position: 'bottom',\n                  child: widget.bottomControl ??\n                      BottomControl(\n                        controller: widget.controller,\n                        triggerFullScreen: _.triggerFullScreen,\n                        buildBottomControl: buildBottomControl(),\n                      ),\n                ),\n              ),\n            ],\n          ),\n        ),\n\n        /// 进度条 live模式下禁用\n\n        Obx(\n          () {\n            final int value = _.sliderPositionSeconds.value;\n            final int max = _.durationSeconds.value;\n            final int buffer = _.bufferedSeconds.value;\n            if (_.showControls.value) {\n              return Container();\n            }\n            if (defaultBtmProgressBehavior ==\n                BtmProgresBehavior.alwaysHide.code) {\n              return const SizedBox();\n            }\n            if (defaultBtmProgressBehavior ==\n                    BtmProgresBehavior.onlyShowFullScreen.code &&\n                !_.isFullScreen.value) {\n              return const SizedBox();\n            } else if (defaultBtmProgressBehavior ==\n                    BtmProgresBehavior.onlyHideFullScreen.code &&\n                _.isFullScreen.value) {\n              return const SizedBox();\n            }\n\n            if (_.videoType == 'live') {\n              return const SizedBox();\n            }\n            if (value > max || max <= 0) {\n              return const SizedBox();\n            }\n            return Positioned(\n              bottom: -1.5,\n              left: 0,\n              right: 0,\n              child: ProgressBar(\n                progress: Duration(seconds: value),\n                buffered: Duration(seconds: buffer),\n                total: Duration(seconds: max),\n                progressBarColor: colorTheme,\n                baseBarColor: Colors.white.withOpacity(0.2),\n                bufferedBarColor:\n                    Theme.of(context).colorScheme.primary.withOpacity(0.4),\n                timeLabelLocation: TimeLabelLocation.none,\n                thumbColor: colorTheme,\n                barHeight: 3,\n                thumbRadius: 0.0,\n                // onDragStart: (duration) {\n                //   _.onChangedSliderStart();\n                // },\n                // onDragEnd: () {\n                //   _.onChangedSliderEnd();\n                // },\n                // onDragUpdate: (details) {\n                //   print(details);\n                // },\n                // onSeek: (duration) {\n                //   feedBack();\n                //   _.onChangedSlider(duration.inSeconds.toDouble());\n                //   _.seekTo(duration);\n                // },\n              ),\n              // SlideTransition(\n              //     position: Tween<Offset>(\n              //       begin: Offset.zero,\n              //       end: const Offset(0, -1),\n              //     ).animate(CurvedAnimation(\n              //       parent: animationController,\n              //       curve: Curves.easeInOut,\n              //     )),\n              //     child: ),\n            );\n          },\n        ),\n\n        // 锁\n        Obx(\n          () => Visibility(\n            visible: _.videoType != 'live' && _.isFullScreen.value,\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: FractionalTranslation(\n                translation: const Offset(1, 0.0),\n                child: Visibility(\n                  visible: _.showControls.value,\n                  child: ComBtn(\n                    icon: Icon(\n                      _.controlsLock.value\n                          ? FontAwesomeIcons.lock\n                          : FontAwesomeIcons.lockOpen,\n                      size: 15,\n                      color: Colors.white,\n                    ),\n                    fuc: () => _.onLockControl(!_.controlsLock.value),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n        //\n        Obx(() {\n          if (_.dataStatus.loading || _.isBuffering.value) {\n            return Center(\n              child: Container(\n                padding: const EdgeInsets.all(30),\n                decoration: const BoxDecoration(\n                  shape: BoxShape.circle,\n                  gradient: RadialGradient(\n                    colors: [Colors.black26, Colors.transparent],\n                  ),\n                ),\n                child: Lottie.asset(\n                  'assets/loading.json',\n                  width: 200,\n                ),\n              ),\n            );\n          } else {\n            return const SizedBox();\n          }\n        }),\n\n        /// 点击 快进/快退\n        Obx(\n          () => Visibility(\n            visible:\n                _mountSeekBackwardButton.value || _mountSeekForwardButton.value,\n            child: Positioned.fill(\n              child: Row(\n                children: [\n                  Expanded(\n                    child: _mountSeekBackwardButton.value\n                        ? TweenAnimationBuilder<double>(\n                            tween: Tween<double>(\n                              begin: 0.0,\n                              end: _hideSeekBackwardButton.value ? 0.0 : 1.0,\n                            ),\n                            duration: const Duration(milliseconds: 150),\n                            builder: (BuildContext context, double value,\n                                    Widget? child) =>\n                                Opacity(\n                              opacity: value,\n                              child: child,\n                            ),\n                            onEnd: () {\n                              if (_hideSeekBackwardButton.value) {\n                                _hideSeekBackwardButton.value = false;\n                                _mountSeekBackwardButton.value = false;\n                              }\n                            },\n                            child: BackwardSeekIndicator(\n                              onChanged: (Duration value) => {},\n                              onSubmitted: (Duration value) {\n                                _hideSeekBackwardButton.value = true;\n                                final Player player =\n                                    widget.controller.videoPlayerController!;\n                                Duration result = player.state.position - value;\n                                result = result.clamp(\n                                  Duration.zero,\n                                  player.state.duration,\n                                );\n                                player.seek(result);\n                                widget.controller.play();\n                              },\n                            ),\n                          )\n                        : const SizedBox(),\n                  ),\n                  Expanded(\n                    child: SizedBox(\n                      width: MediaQuery.sizeOf(context).width / 4,\n                    ),\n                  ),\n                  Expanded(\n                    child: _mountSeekForwardButton.value\n                        ? TweenAnimationBuilder<double>(\n                            tween: Tween<double>(\n                              begin: 0.0,\n                              end: _hideSeekForwardButton.value ? 0.0 : 1.0,\n                            ),\n                            duration: const Duration(milliseconds: 150),\n                            builder: (BuildContext context, double value,\n                                    Widget? child) =>\n                                Opacity(\n                              opacity: value,\n                              child: child,\n                            ),\n                            onEnd: () {\n                              if (_hideSeekForwardButton.value) {\n                                _hideSeekForwardButton.value = false;\n                                _mountSeekForwardButton.value = false;\n                              }\n                            },\n                            child: ForwardSeekIndicator(\n                              onChanged: (Duration value) => {},\n                              onSubmitted: (Duration value) {\n                                _hideSeekForwardButton.value = true;\n                                final Player player =\n                                    widget.controller.videoPlayerController!;\n                                Duration result = player.state.position + value;\n                                result = result.clamp(\n                                  Duration.zero,\n                                  player.state.duration,\n                                );\n                                player.seek(result);\n                                widget.controller.play();\n                              },\n                            ),\n                          )\n                        : const SizedBox(),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/widgets/app_bar_ani.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppBarAni extends StatelessWidget implements PreferredSizeWidget {\n  const AppBarAni({\n    required this.child,\n    required this.controller,\n    required this.visible,\n    this.position,\n    Key? key,\n  }) : super(key: key);\n\n  final PreferredSizeWidget child;\n  final AnimationController controller;\n  final bool visible;\n  final String? position;\n\n  @override\n  Size get preferredSize => child.preferredSize;\n\n  @override\n  Widget build(BuildContext context) {\n    visible ? controller.forward() : controller.reverse();\n    return SlideTransition(\n      position: Tween<Offset>(\n        begin: Offset(0, position! == 'top' ? -1 : 1),\n        end: Offset.zero,\n      ).animate(CurvedAnimation(\n        parent: controller,\n        curve: Curves.linear,\n      )),\n      child: Container(\n        padding: EdgeInsets.only(\n          left: MediaQuery.of(context).padding.left,\n          right: MediaQuery.of(context).padding.right,\n        ),\n        decoration: BoxDecoration(\n          gradient: position! == 'top'\n              ? const LinearGradient(\n                  begin: Alignment.bottomCenter,\n                  end: Alignment.topCenter,\n                  colors: <Color>[\n                    Colors.transparent,\n                    Colors.black54,\n                  ],\n                  tileMode: TileMode.mirror,\n                )\n              : const LinearGradient(\n                  begin: Alignment.topCenter,\n                  end: Alignment.bottomCenter,\n                  colors: <Color>[\n                    Colors.transparent,\n                    Colors.black54,\n                  ],\n                  tileMode: TileMode.mirror,\n                ),\n        ),\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/widgets/backward_seek.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\n\nclass BackwardSeekIndicator extends StatefulWidget {\n  final void Function(Duration) onChanged;\n  final void Function(Duration) onSubmitted;\n  const BackwardSeekIndicator({\n    Key? key,\n    required this.onChanged,\n    required this.onSubmitted,\n  }) : super(key: key);\n\n  @override\n  State<BackwardSeekIndicator> createState() => BackwardSeekIndicatorState();\n}\n\nclass BackwardSeekIndicatorState extends State<BackwardSeekIndicator> {\n  Duration value = const Duration(seconds: 10);\n\n  Timer? timer;\n\n  @override\n  void setState(VoidCallback fn) {\n    if (mounted) {\n      super.setState(fn);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    timer = Timer(const Duration(milliseconds: 200), () {\n      widget.onSubmitted.call(value);\n    });\n  }\n\n  void increment() {\n    timer?.cancel();\n    timer = Timer(const Duration(milliseconds: 200), () {\n      widget.onSubmitted.call(value);\n    });\n    widget.onChanged.call(value);\n    // 重复点击 快退秒数累加10\n    setState(() {\n      value += const Duration(seconds: 10);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: const BoxDecoration(\n        gradient: LinearGradient(\n          colors: [\n            Color(0x88767676),\n            Color(0x00767676),\n          ],\n          begin: Alignment.centerLeft,\n          end: Alignment.centerRight,\n        ),\n      ),\n      child: InkWell(\n        splashColor: const Color(0x44767676),\n        onTap: increment,\n        child: Center(\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            crossAxisAlignment: CrossAxisAlignment.center,\n            children: [\n              const Icon(\n                Icons.fast_rewind,\n                size: 24.0,\n                color: Color(0xFFFFFFFF),\n              ),\n              const SizedBox(height: 8.0),\n              Text(\n                '快退${value.inSeconds}秒',\n                style: const TextStyle(\n                  fontSize: 12.0,\n                  color: Color(0xFFFFFFFF),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/widgets/bottom_control.dart",
    "content": "import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/utils/feed_back.dart';\n\nclass BottomControl extends StatelessWidget implements PreferredSizeWidget {\n  final PlPlayerController? controller;\n  final Function? triggerFullScreen;\n  final List<Widget>? buildBottomControl;\n  const BottomControl({\n    this.controller,\n    this.triggerFullScreen,\n    this.buildBottomControl,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Size get preferredSize => const Size(double.infinity, kToolbarHeight);\n\n  @override\n  Widget build(BuildContext context) {\n    Color colorTheme = Theme.of(context).colorScheme.primary;\n    final _ = controller!;\n    return Container(\n      color: Colors.transparent,\n      height: 90,\n      padding: const EdgeInsets.only(left: 18, right: 18),\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.end,\n        children: [\n          Obx(\n            () {\n              final int value = _.sliderPositionSeconds.value;\n              final int max = _.durationSeconds.value;\n              final int buffer = _.bufferedSeconds.value;\n              if (value > max || max <= 0) {\n                return const SizedBox();\n              }\n              return Padding(\n                padding: const EdgeInsets.only(left: 7, right: 7, bottom: 6),\n                child: ProgressBar(\n                  progress: Duration(seconds: value),\n                  buffered: Duration(seconds: buffer),\n                  total: Duration(seconds: max),\n                  progressBarColor: colorTheme,\n                  baseBarColor: Colors.white.withOpacity(0.2),\n                  bufferedBarColor: colorTheme.withOpacity(0.4),\n                  timeLabelLocation: TimeLabelLocation.none,\n                  thumbColor: colorTheme,\n                  barHeight: 3.5,\n                  thumbRadius: 7,\n                  onDragStart: (duration) {\n                    feedBack();\n                    _.onChangedSliderStart();\n                  },\n                  onDragUpdate: (duration) {\n                    _.onUpdatedSliderProgress(duration.timeStamp);\n                  },\n                  onSeek: (duration) {\n                    _.onChangedSliderEnd();\n                    _.onChangedSlider(duration.inSeconds.toDouble());\n                    _.seekTo(Duration(seconds: duration.inSeconds),\n                        type: 'slider');\n                  },\n                ),\n              );\n            },\n          ),\n          Row(children: [...buildBottomControl!]),\n          const SizedBox(height: 10),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/widgets/common_btn.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ComBtn extends StatelessWidget {\n  final Widget? icon;\n  final Function? fuc;\n\n  const ComBtn({\n    this.icon,\n    this.fuc,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 34,\n      height: 34,\n      child: IconButton(\n        style: ButtonStyle(\n          padding: MaterialStateProperty.all(EdgeInsets.zero),\n        ),\n        onPressed: () {\n          fuc!();\n        },\n        icon: icon!,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/widgets/control_bar.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ControlBar extends StatelessWidget {\n  final bool visible;\n  final IconData icon;\n  final double value;\n\n  const ControlBar({\n    Key? key,\n    required this.visible,\n    required this.icon,\n    required this.value,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    Color color = const Color(0xFFFFFFFF);\n    return Align(\n      child: AnimatedOpacity(\n        curve: Curves.easeInOut,\n        opacity: visible ? 1.0 : 0.0,\n        duration: const Duration(milliseconds: 150),\n        child: IntrinsicWidth(\n          child: Container(\n            padding: const EdgeInsets.fromLTRB(10, 2, 10, 2),\n            decoration: BoxDecoration(\n              color: const Color(0x88000000),\n              borderRadius: BorderRadius.circular(64.0),\n            ),\n            height: 34.0,\n            child: Row(\n              children: <Widget>[\n                Icon(icon, color: color, size: 18.0),\n                const SizedBox(width: 4.0),\n                Container(\n                  constraints: const BoxConstraints(minWidth: 30.0),\n                  child: Text(\n                    '${(value * 100.0).round()}%',\n                    textAlign: TextAlign.center,\n                    style: TextStyle(fontSize: 13.0, color: color),\n                  ),\n                )\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/widgets/forward_seek.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\n\nclass ForwardSeekIndicator extends StatefulWidget {\n  final void Function(Duration) onChanged;\n  final void Function(Duration) onSubmitted;\n  const ForwardSeekIndicator({\n    Key? key,\n    required this.onChanged,\n    required this.onSubmitted,\n  }) : super(key: key);\n\n  @override\n  State<ForwardSeekIndicator> createState() => ForwardSeekIndicatorState();\n}\n\nclass ForwardSeekIndicatorState extends State<ForwardSeekIndicator> {\n  Duration value = const Duration(seconds: 10);\n\n  Timer? timer;\n\n  @override\n  void setState(VoidCallback fn) {\n    if (mounted) {\n      super.setState(fn);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    timer = Timer(const Duration(milliseconds: 200), () {\n      widget.onSubmitted.call(value);\n    });\n  }\n\n  void increment() {\n    timer?.cancel();\n    timer = Timer(const Duration(milliseconds: 200), () {\n      widget.onSubmitted.call(value);\n    });\n    widget.onChanged.call(value);\n    // 重复点击 快进秒数累加10\n    setState(() {\n      value += const Duration(seconds: 10);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: const BoxDecoration(\n        gradient: LinearGradient(\n          colors: [\n            Color(0x00767676),\n            Color(0x88767676),\n          ],\n          begin: Alignment.centerLeft,\n          end: Alignment.centerRight,\n        ),\n      ),\n      child: InkWell(\n        splashColor: const Color(0x44767676),\n        onTap: increment,\n        child: Center(\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            mainAxisAlignment: MainAxisAlignment.center,\n            crossAxisAlignment: CrossAxisAlignment.center,\n            children: [\n              const Icon(\n                Icons.fast_forward,\n                size: 24.0,\n                color: Color(0xFFFFFFFF),\n              ),\n              const SizedBox(height: 8.0),\n              Text(\n                '快进${value.inSeconds}秒',\n                style: const TextStyle(\n                  fontSize: 12.0,\n                  color: Color(0xFFFFFFFF),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_player/widgets/play_pause_btn.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:media_kit/media_kit.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\n\nclass PlayOrPauseButton extends StatefulWidget {\n  final double? iconSize;\n  final Color? iconColor;\n  final PlPlayerController? controller;\n\n  const PlayOrPauseButton({\n    super.key,\n    this.iconSize,\n    this.iconColor,\n    this.controller,\n  });\n\n  @override\n  PlayOrPauseButtonState createState() => PlayOrPauseButtonState();\n}\n\nclass PlayOrPauseButtonState extends State<PlayOrPauseButton>\n    with SingleTickerProviderStateMixin {\n  late final AnimationController animation;\n\n  StreamSubscription<bool>? subscription;\n  late Player player;\n  bool isOpacity = false;\n\n  @override\n  void initState() {\n    super.initState();\n    player = widget.controller!.videoPlayerController!;\n    animation = AnimationController(\n      vsync: this,\n      value: player.state.playing ? 1 : 0,\n      duration: const Duration(milliseconds: 200),\n    );\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    subscription ??= player.stream.playing.listen((event) {\n      if (event) {\n        animation.forward().then((value) => {\n              isOpacity = true,\n            });\n      } else {\n        animation.reverse().then((value) => {isOpacity = false});\n      }\n      setState(() {});\n    });\n  }\n\n  @override\n  void dispose() {\n    animation.dispose();\n    subscription?.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 34,\n      height: 34,\n      child: IconButton(\n        style: ButtonStyle(\n          padding: MaterialStateProperty.all(EdgeInsets.zero),\n        ),\n        onPressed: player.playOrPause,\n        color: Colors.white,\n        iconSize: 20,\n        // iconSize: widget.iconSize ?? _theme(context).buttonBarButtonSize,\n        // color: widget.iconColor ?? _theme(context).buttonBarButtonColor,\n        icon: AnimatedIcon(\n          progress: animation,\n          icon: AnimatedIcons.play_pause,\n          color: Colors.white,\n          size: 20,\n          // size: widget.iconSize ?? _theme(context).buttonBarButtonSize,\n          // color: widget.iconColor ?? _theme(context).buttonBarButtonColor,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_popup/index.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass PlPopupRoute extends PopupRoute<void> {\n  PlPopupRoute({\n    this.backgroudColor,\n    this.alignment = Alignment.center,\n    required this.child,\n    this.onClick,\n  });\n\n  /// backgroudColor\n  final Color? backgroudColor;\n\n  /// child'alignment, default value: [Alignment.center]\n  final Alignment alignment;\n\n  /// child\n  final Widget child;\n\n  /// backgroudView action\n  final Function? onClick;\n\n  @override\n  Duration get transitionDuration => const Duration(milliseconds: 300);\n\n  @override\n  bool get barrierDismissible => false;\n\n  @override\n  Color get barrierColor => Colors.black54;\n\n  @override\n  String? get barrierLabel => null;\n\n  @override\n  Widget buildPage(\n    BuildContext context,\n    Animation<double> animation,\n    Animation<double> secondaryAnimation,\n  ) {\n    return child;\n  }\n}\n"
  },
  {
    "path": "lib/plugin/pl_socket/index.dart",
    "content": "import 'dart:async';\n\nimport 'package:pilipala/utils/live.dart';\nimport 'package:web_socket_channel/io.dart';\nimport 'package:web_socket_channel/web_socket_channel.dart';\n\nenum SocketStatus {\n  connected,\n  failed,\n  closed,\n}\n\nclass PlSocket {\n  SocketStatus status = SocketStatus.closed;\n  // 链接\n  final String url;\n  // 心跳时间\n  final int heartTime;\n  // 监听初始化完成\n  final Function? onReadyCb;\n  // 监听关闭\n  final Function? onCloseCb;\n  // 监听异常\n  final Function? onErrorCb;\n  // 监听消息\n  final Function? onMessageCb;\n  // 请求头\n  final Map<String, dynamic>? headers;\n\n  PlSocket({\n    required this.url,\n    required this.heartTime,\n    this.onReadyCb,\n    this.onCloseCb,\n    this.onErrorCb,\n    this.onMessageCb,\n    this.headers,\n  });\n\n  WebSocketChannel? channel;\n  StreamSubscription<dynamic>? channelStreamSub;\n\n  // 建立连接\n  Future connect() async {\n    // 连接之前关闭上次连接\n    onClose();\n    try {\n      channel = IOWebSocketChannel.connect(\n        url,\n        connectTimeout: const Duration(seconds: 15),\n        headers: null,\n      );\n      await channel?.ready;\n      onReady();\n    } catch (err) {\n      connect();\n      onError(err);\n    }\n  }\n\n  // 初始化完成\n  void onReady() {\n    status = SocketStatus.connected;\n    onReadyCb?.call();\n    channelStreamSub = channel?.stream.listen((message) {\n      onMessageCb?.call(message);\n    }, onDone: () {\n      // 流被关闭\n      print('结束了');\n    }, onError: (err) {\n      onError(err);\n    });\n    // 每30s发送心跳\n    Timer.periodic(Duration(seconds: heartTime), (timer) {\n      if (status == SocketStatus.connected) {\n        sendMessage(LiveUtils.encodeData(\n          \"\",\n          2,\n        ));\n      } else {\n        timer.cancel();\n      }\n    });\n  }\n\n  // 连接关闭\n  void onClose() {\n    status = SocketStatus.closed;\n    onCloseCb?.call();\n    channelStreamSub?.cancel();\n    channel?.sink.close();\n  }\n\n  // 连接异常\n  void onError(err) {\n    onErrorCb?.call(err);\n  }\n\n  // 接收消息\n  void onMessage() {}\n\n  void sendMessage(dynamic message) {\n    if (status == SocketStatus.connected) {\n      channel?.sink.add(message);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/router/app_pages.dart",
    "content": "// ignore_for_file: must_be_immutable\n\nimport 'package:flutter/material.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/pages/fav_edit/index.dart';\nimport 'package:pilipala/pages/follow_search/view.dart';\nimport 'package:pilipala/pages/member_article/index.dart';\nimport 'package:pilipala/pages/message/at/index.dart';\nimport 'package:pilipala/pages/message/like/index.dart';\nimport 'package:pilipala/pages/message/reply/index.dart';\nimport 'package:pilipala/pages/message/system/index.dart';\nimport 'package:pilipala/pages/opus/index.dart';\nimport 'package:pilipala/pages/read/index.dart';\nimport 'package:pilipala/pages/setting/pages/logs.dart';\n\nimport '../pages/about/index.dart';\nimport '../pages/blacklist/index.dart';\nimport '../pages/dynamics/detail/index.dart';\nimport '../pages/dynamics/index.dart';\nimport '../pages/fan/index.dart';\nimport '../pages/fav/index.dart';\nimport '../pages/fav_detail/index.dart';\nimport '../pages/fav_search/index.dart';\nimport '../pages/follow/index.dart';\nimport '../pages/history/index.dart';\nimport '../pages/history_search/index.dart';\nimport '../pages/home/index.dart';\nimport '../pages/hot/index.dart';\nimport '../pages/html/index.dart';\nimport '../pages/later/index.dart';\nimport '../pages/live_room/view.dart';\nimport '../pages/login/index.dart';\nimport '../pages/media/index.dart';\nimport '../pages/member/index.dart';\nimport '../pages/member_archive/index.dart';\nimport '../pages/member_coin/index.dart';\nimport '../pages/member_dynamics/index.dart';\nimport '../pages/member_like/index.dart';\nimport '../pages/member_search/index.dart';\nimport '../pages/member_seasons/index.dart';\nimport '../pages/search/index.dart';\nimport '../pages/search_result/index.dart';\nimport '../pages/setting/extra_setting.dart';\nimport '../pages/setting/index.dart';\nimport '../pages/setting/pages/action_menu_set.dart';\nimport '../pages/setting/pages/color_select.dart';\nimport '../pages/setting/pages/display_mode.dart';\nimport '../pages/setting/pages/font_size_select.dart';\nimport '../pages/setting/pages/home_tabbar_set.dart';\nimport '../pages/setting/pages/navigation_bar_set.dart';\nimport '../pages/setting/pages/play_gesture_set.dart';\nimport '../pages/setting/pages/play_speed_set.dart';\nimport '../pages/setting/recommend_setting.dart';\nimport '../pages/setting/play_setting.dart';\nimport '../pages/setting/privacy_setting.dart';\nimport '../pages/setting/style_setting.dart';\nimport '../pages/subscription/index.dart';\nimport '../pages/subscription_detail/index.dart';\nimport '../pages/video/detail/index.dart';\nimport '../pages/video/detail/reply_reply/index.dart';\nimport '../pages/webview/index.dart';\nimport '../pages/whisper/index.dart';\nimport '../pages/whisper_detail/index.dart';\nimport '../utils/storage.dart';\n\nBox<dynamic> setting = GStrorage.setting;\n\nclass Routes {\n  static final List<GetPage<dynamic>> getPages = [\n    // 首页(推荐)\n    CustomGetPage(name: '/', page: () => const HomePage()),\n    // 热门\n    CustomGetPage(name: '/hot', page: () => const HotPage()),\n    // 视频详情\n    CustomGetPage(name: '/video', page: () => const VideoDetailPage()),\n    //\n    CustomGetPage(name: '/webview', page: () => const WebviewPage()),\n    // 设置\n    CustomGetPage(name: '/setting', page: () => const SettingPage()),\n    //\n    CustomGetPage(name: '/media', page: () => const MediaPage()),\n    //\n    CustomGetPage(name: '/fav', page: () => const FavPage()),\n    //\n    CustomGetPage(name: '/favDetail', page: () => const FavDetailPage()),\n    // 稍后再看\n    CustomGetPage(name: '/later', page: () => const LaterPage()),\n    // 历史记录\n    CustomGetPage(name: '/history', page: () => const HistoryPage()),\n    // 搜索页面\n    CustomGetPage(name: '/search', page: () => const SearchPage()),\n    // 搜索结果\n    CustomGetPage(name: '/searchResult', page: () => const SearchResultPage()),\n    // 动态\n    CustomGetPage(name: '/dynamics', page: () => const DynamicsPage()),\n    // 动态详情\n    CustomGetPage(\n        name: '/dynamicDetail', page: () => const DynamicDetailPage()),\n    // 关注\n    CustomGetPage(name: '/follow', page: () => const FollowPage()),\n    // 粉丝\n    CustomGetPage(name: '/fan', page: () => const FansPage()),\n    // 直播详情\n    CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()),\n    // 用户中心\n    CustomGetPage(name: '/member', page: () => const MemberPage()),\n    CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()),\n    // 二级回复\n    CustomGetPage(\n        name: '/replyReply', page: () => const VideoReplyReplyPanel()),\n    // 推荐设置\n    CustomGetPage(\n        name: '/recommendSetting', page: () => const RecommendSetting()),\n    // 播放设置\n    CustomGetPage(name: '/playSetting', page: () => const PlaySetting()),\n    // 外观设置\n    CustomGetPage(name: '/styleSetting', page: () => const StyleSetting()),\n    // 隐私设置\n    CustomGetPage(name: '/privacySetting', page: () => const PrivacySetting()),\n    // 其他设置\n    CustomGetPage(name: '/extraSetting', page: () => const ExtraSetting()),\n    //\n    CustomGetPage(name: '/blackListPage', page: () => const BlackListPage()),\n    CustomGetPage(name: '/colorSetting', page: () => const ColorSelectPage()),\n    // 首页tabbar\n    CustomGetPage(name: '/tabbarSetting', page: () => const TabbarSetPage()),\n    CustomGetPage(\n        name: '/fontSizeSetting', page: () => const FontSizeSelectPage()),\n    // 屏幕帧率\n    CustomGetPage(\n        name: '/displayModeSetting', page: () => const SetDiaplayMode()),\n    // 关于\n    CustomGetPage(name: '/about', page: () => const AboutPage()),\n    //\n    CustomGetPage(name: '/htmlRender', page: () => const HtmlRenderPage()),\n    // 历史记录搜索\n    CustomGetPage(\n        name: '/historySearch', page: () => const HistorySearchPage()),\n\n    CustomGetPage(name: '/playSpeedSet', page: () => const PlaySpeedPage()),\n    // 收藏搜索\n    CustomGetPage(name: '/favSearch', page: () => const FavSearchPage()),\n    // 消息页面\n    CustomGetPage(name: '/whisper', page: () => const WhisperPage()),\n    // 私信详情\n    CustomGetPage(\n        name: '/whisperDetail', page: () => const WhisperDetailPage()),\n    // 登录页面\n    CustomGetPage(name: '/loginPage', page: () => const LoginPage()),\n    // 用户动态\n    CustomGetPage(\n        name: '/memberDynamics', page: () => const MemberDynamicsPage()),\n    // 用户投稿\n    CustomGetPage(\n        name: '/memberArchive', page: () => const MemberArchivePage()),\n    // 用户最近投币\n    CustomGetPage(name: '/memberCoin', page: () => const MemberCoinPage()),\n    // 用户最近喜欢\n    CustomGetPage(name: '/memberLike', page: () => const MemberLikePage()),\n    // 用户专栏\n    CustomGetPage(\n        name: '/memberSeasons', page: () => const MemberSeasonsPage()),\n    // 日志\n    CustomGetPage(name: '/logs', page: () => const LogsPage()),\n    // 搜索关注\n    CustomGetPage(name: '/followSearch', page: () => const FollowSearchPage()),\n    // 订阅\n    CustomGetPage(name: '/subscription', page: () => const SubPage()),\n    // 订阅详情\n    CustomGetPage(name: '/subDetail', page: () => const SubDetailPage()),\n    // 播放器手势\n    CustomGetPage(\n        name: '/playerGestureSet', page: () => const PlayGesturePage()),\n    // navigation bar\n    CustomGetPage(\n        name: '/navbarSetting', page: () => const NavigationBarSetPage()),\n    // 操作菜单\n    CustomGetPage(\n        name: '/actionMenuSet', page: () => const ActionMenuSetPage()),\n    // 回复我的\n    CustomGetPage(name: '/messageReply', page: () => const MessageReplyPage()),\n    // @我的\n    CustomGetPage(name: '/messageAt', page: () => const MessageAtPage()),\n    // 收到的赞\n    CustomGetPage(name: '/messageLike', page: () => const MessageLikePage()),\n    // 系统通知\n    CustomGetPage(\n        name: '/messageSystem', page: () => const MessageSystemPage()),\n    // 收藏夹编辑\n    CustomGetPage(name: '/favEdit', page: () => const FavEditPage()),\n\n    // 专栏\n    CustomGetPage(name: '/opus', page: () => const OpusPage()),\n    CustomGetPage(name: '/read', page: () => const ReadPage()),\n    // 用户专栏\n    CustomGetPage(\n        name: '/memberArticle', page: () => const MemberArticlePage()),\n  ];\n}\n\nclass CustomGetPage extends GetPage<dynamic> {\n  CustomGetPage({\n    required super.name,\n    required super.page,\n    this.fullscreen,\n    super.transitionDuration,\n  }) : super(\n          curve: Curves.linear,\n          transition: Transition.native,\n          showCupertinoParallax: false,\n          popGesture: false,\n          fullscreenDialog: fullscreen != null && fullscreen,\n        );\n  bool? fullscreen = false;\n}\n"
  },
  {
    "path": "lib/scripts/build.sh",
    "content": "flutter build apk --target-platform android-arm64 --split-per-abi"
  },
  {
    "path": "lib/services/audio_handler.dart",
    "content": "import 'package:audio_service/audio_service.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/models/bangumi/info.dart';\nimport 'package:pilipala/models/video_detail_res.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nFuture<VideoPlayerServiceHandler> initAudioService() async {\n  return await AudioService.init(\n    builder: () => VideoPlayerServiceHandler(),\n    config: const AudioServiceConfig(\n      androidNotificationChannelId: 'com.guozhigq.pilipala.audio',\n      androidNotificationChannelName: 'Audio Service Pilipala',\n      androidNotificationOngoing: true,\n      androidStopForegroundOnPause: true,\n      fastForwardInterval: Duration(seconds: 10),\n      rewindInterval: Duration(seconds: 10),\n      androidNotificationChannelDescription: 'Media notification channel',\n      androidNotificationIcon: 'drawable/ic_notification_icon',\n    ),\n  );\n}\n\nclass VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {\n  static final List<MediaItem> _item = [];\n  Box setting = GStrorage.setting;\n  bool enableBackgroundPlay = false;\n  PlPlayerController player = PlPlayerController();\n\n  VideoPlayerServiceHandler() {\n    revalidateSetting();\n  }\n\n  revalidateSetting() {\n    enableBackgroundPlay =\n        setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);\n  }\n\n  @override\n  Future<void> play() async {\n    player.play();\n  }\n\n  @override\n  Future<void> pause() async {\n    player.pause();\n  }\n\n  @override\n  Future<void> seek(Duration position) async {\n    playbackState.add(playbackState.value.copyWith(\n      updatePosition: position,\n    ));\n    await player.seekTo(position);\n  }\n\n  Future<void> setMediaItem(MediaItem newMediaItem) async {\n    if (!enableBackgroundPlay) return;\n    mediaItem.add(newMediaItem);\n  }\n\n  Future<void> setPlaybackState(PlayerStatus status, bool isBuffering) async {\n    if (!enableBackgroundPlay) return;\n\n    final AudioProcessingState processingState;\n    final playing = status == PlayerStatus.playing;\n    if (status == PlayerStatus.completed) {\n      processingState = AudioProcessingState.completed;\n    } else if (isBuffering) {\n      processingState = AudioProcessingState.buffering;\n    } else {\n      processingState = AudioProcessingState.ready;\n    }\n\n    playbackState.add(playbackState.value.copyWith(\n      processingState:\n          isBuffering ? AudioProcessingState.buffering : processingState,\n      controls: [\n        MediaControl.rewind\n            .copyWith(androidIcon: 'drawable/ic_baseline_replay_10_24'),\n        if (playing) MediaControl.pause else MediaControl.play,\n        MediaControl.fastForward\n            .copyWith(androidIcon: 'drawable/ic_baseline_forward_10_24'),\n      ],\n      playing: playing,\n      systemActions: const {\n        MediaAction.seek,\n      },\n    ));\n  }\n\n  onStatusChange(PlayerStatus status, bool isBuffering) {\n    if (!enableBackgroundPlay) return;\n\n    if (_item.isEmpty) return;\n    setPlaybackState(status, isBuffering);\n  }\n\n  onVideoDetailChange(dynamic data, int cid) {\n    if (!enableBackgroundPlay) return;\n\n    if (data == null) return;\n    Map argMap = Get.arguments;\n    final heroTag = argMap['heroTag'];\n\n    late MediaItem? mediaItem;\n    if (data is VideoDetailData) {\n      if ((data.pages?.length ?? 0) > 1) {\n        final current = data.pages?.firstWhere((element) => element.cid == cid);\n        mediaItem = MediaItem(\n          id: heroTag,\n          title: current?.pagePart ?? \"\",\n          artist: data.title ?? \"\",\n          album: data.title ?? \"\",\n          duration: Duration(seconds: current?.duration ?? 0),\n          artUri: Uri.parse(data.pic ?? \"\"),\n        );\n      } else {\n        mediaItem = MediaItem(\n          id: heroTag,\n          title: data.title ?? \"\",\n          artist: data.owner?.name ?? \"\",\n          duration: Duration(seconds: data.duration ?? 0),\n          artUri: Uri.parse(data.pic ?? \"\"),\n        );\n      }\n    } else if (data is BangumiInfoModel) {\n      final current =\n          data.episodes?.firstWhere((element) => element.cid == cid);\n      mediaItem = MediaItem(\n        id: heroTag,\n        title: current?.longTitle ?? \"\",\n        artist: data.title ?? \"\",\n        duration: Duration(milliseconds: current?.duration ?? 0),\n        artUri: Uri.parse(data.cover ?? \"\"),\n      );\n    }\n    if (mediaItem == null) return;\n    setMediaItem(mediaItem);\n    _item.add(mediaItem);\n  }\n\n  onVideoDetailDispose() {\n    if (!enableBackgroundPlay) return;\n\n    playbackState.add(playbackState.value.copyWith(\n      processingState: AudioProcessingState.idle,\n      playing: false,\n    ));\n    if (_item.isNotEmpty) {\n      _item.removeLast();\n    }\n    if (_item.isNotEmpty) {\n      setMediaItem(_item.last);\n    }\n    if (_item.isEmpty) {\n      playbackState\n          .add(playbackState.value.copyWith(updatePosition: Duration.zero));\n    }\n    stop();\n  }\n\n  clear() {\n    if (!enableBackgroundPlay) return;\n\n    mediaItem.add(null);\n    playbackState.add(PlaybackState(\n      processingState: AudioProcessingState.idle,\n      playing: false,\n    ));\n    _item.clear();\n    stop();\n  }\n\n  onPositionChange(Duration position) {\n    if (!enableBackgroundPlay) return;\n\n    playbackState.add(playbackState.value.copyWith(\n      updatePosition: position,\n    ));\n  }\n}\n"
  },
  {
    "path": "lib/services/audio_session.dart",
    "content": "import 'package:audio_session/audio_session.dart';\nimport 'package:pilipala/plugin/pl_player/index.dart';\n\nclass AudioSessionHandler {\n  late AudioSession session;\n  bool _playInterrupted = false;\n\n  setActive(bool active) {\n    session.setActive(active);\n  }\n\n  AudioSessionHandler() {\n    initSession();\n  }\n\n  Future<void> initSession() async {\n    session = await AudioSession.instance;\n    session.configure(const AudioSessionConfiguration.music());\n\n    session.interruptionEventStream.listen((event) {\n      final player = PlPlayerController(videoType: 'none');\n      if (event.begin) {\n        if (!player.playerStatus.playing) return;\n        switch (event.type) {\n          case AudioInterruptionType.duck:\n            player.setVolume(player.volume.value * 0.5);\n            break;\n          case AudioInterruptionType.pause:\n            player.pause(isInterrupt: true);\n            _playInterrupted = true;\n            break;\n          case AudioInterruptionType.unknown:\n            player.pause(isInterrupt: true);\n            _playInterrupted = true;\n            break;\n        }\n      } else {\n        switch (event.type) {\n          case AudioInterruptionType.duck:\n            player.setVolume(player.volume.value * 2);\n            break;\n          case AudioInterruptionType.pause:\n            if (_playInterrupted) player.play();\n            break;\n          case AudioInterruptionType.unknown:\n            break;\n        }\n        _playInterrupted = false;\n      }\n    });\n\n    // 耳机拔出暂停\n    session.becomingNoisyEventStream.listen((_) {\n      final player = PlPlayerController(videoType: 'none');\n      if (player.playerStatus.playing) {\n        player.pause();\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "lib/services/disable_battery_opt.dart",
    "content": "import 'dart:io';\n\nimport 'package:disable_battery_optimization/disable_battery_optimization.dart';\nimport 'package:pilipala/utils/storage.dart';\n\nvoid DisableBatteryOpt() async {\n  if (!Platform.isAndroid) {\n    return;\n  }\n  // 本地缓存中读取 是否禁用了电池优化 默认未禁用\n  bool isDisableBatteryOptLocal =\n      GStrorage.localCache.get('isDisableBatteryOptLocal', defaultValue: false);\n  if (!isDisableBatteryOptLocal) {\n    final isBatteryOptimizationDisabled =\n        await DisableBatteryOptimization.isBatteryOptimizationDisabled;\n    if (isBatteryOptimizationDisabled == false) {\n      final hasDisabled = await DisableBatteryOptimization\n          .showDisableBatteryOptimizationSettings();\n      // 设置为已禁用\n      GStrorage.localCache.put('isDisableBatteryOptLocal', hasDisabled == true);\n    }\n  }\n\n  bool isManufacturerBatteryOptimizationDisabled = GStrorage.localCache\n      .get('isManufacturerBatteryOptimizationDisabled', defaultValue: false);\n  if (!isManufacturerBatteryOptimizationDisabled) {\n    final isManBatteryOptimizationDisabled = await DisableBatteryOptimization\n        .isManufacturerBatteryOptimizationDisabled;\n    if (isManBatteryOptimizationDisabled == false) {\n      final hasDisabled = await DisableBatteryOptimization\n          .showDisableManufacturerBatteryOptimizationSettings(\n        \"当前设备可能有额外的电池优化\",\n        \"按照步骤操作以禁用电池优化，以保证应用在后台正常运行\",\n      );\n      // 设置为已禁用\n      GStrorage.localCache.put(\n          'isManufacturerBatteryOptimizationDisabled', hasDisabled == true);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/services/loggeer.dart",
    "content": "// final _loggerFactory =\n\nimport 'dart:io';\n\nimport 'package:logger/logger.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:path/path.dart' as p;\n\nfinal _loggerFactory = PiliLogger();\n\nPiliLogger getLogger<T>() {\n  return _loggerFactory;\n}\n\nclass PiliLogger extends Logger {\n  PiliLogger() : super();\n\n  @override\n  void log(Level level, dynamic message,\n      {Object? error, StackTrace? stackTrace, DateTime? time}) async {\n    if (level == Level.error) {\n      String dir = (await getApplicationDocumentsDirectory()).path;\n      // 创建logo文件\n      final String filename = p.join(dir, \".pili_logs\");\n      // 添加至文件末尾\n      await File(filename).writeAsString(\n        \"**${DateTime.now()}** \\n $message \\n $stackTrace\",\n        mode: FileMode.writeOnlyAppend,\n      );\n    }\n    super.log(level, \"$message\", error: error, stackTrace: stackTrace);\n  }\n}\n\nFuture<File> getLogsPath() async {\n  String dir = (await getApplicationDocumentsDirectory()).path;\n  final String filename = p.join(dir, \".pili_logs\");\n  final file = File(filename);\n  if (!await file.exists()) {\n    await file.create();\n  }\n  return file;\n}\n\nFuture<bool> clearLogs() async {\n  String dir = (await getApplicationDocumentsDirectory()).path;\n  final String filename = p.join(dir, \".pili_logs\");\n  final file = File(filename);\n  try {\n    await file.writeAsString('');\n  } catch (e) {\n    print('Error clearing file: $e');\n    return false;\n  }\n  return true;\n}\n"
  },
  {
    "path": "lib/services/service_locator.dart",
    "content": "import 'audio_handler.dart';\nimport 'audio_session.dart';\n\nlate VideoPlayerServiceHandler videoPlayerServiceHandler;\nlate AudioSessionHandler audioSessionHandler;\n\nFuture<void> setupServiceLocator() async {\n  final audio = await initAudioService();\n  videoPlayerServiceHandler = audio;\n  audioSessionHandler = AudioSessionHandler();\n}\n"
  },
  {
    "path": "lib/services/shutdown_timer_service.dart",
    "content": "// 定时关闭服务\nimport 'dart:async';\nimport 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\n\nimport '../plugin/pl_player/controller.dart';\n\nclass ShutdownTimerService {\n  static final ShutdownTimerService _instance =\n      ShutdownTimerService._internal();\n  Timer? _shutdownTimer;\n  Timer? _autoCloseDialogTimer;\n  //定时退出\n  int scheduledExitInMinutes = -1;\n  bool exitApp = false;\n  bool waitForPlayingCompleted = false;\n  bool isWaiting = false;\n\n  factory ShutdownTimerService() => _instance;\n\n  ShutdownTimerService._internal();\n\n  void startShutdownTimer() {\n    cancelShutdownTimer(); // Cancel any previous timer\n    if (scheduledExitInMinutes == -1) {\n      //使用toast提示用户已取消\n      SmartDialog.showToast(\"取消定时关闭\");\n      return;\n    }\n    SmartDialog.showToast(\"设置 $scheduledExitInMinutes 分钟后定时关闭\");\n    _shutdownTimer = Timer(\n        Duration(minutes: scheduledExitInMinutes), () => _shutdownDecider());\n  }\n\n  void _showTimeUpButPauseDialog() {\n    SmartDialog.show(\n      builder: (BuildContext dialogContext) {\n        return AlertDialog(\n          title: const Text('定时关闭'),\n          content: const Text('时间到啦！'),\n          actions: <Widget>[\n            TextButton(\n              child: const Text('确认'),\n              onPressed: () {\n                cancelShutdownTimer();\n                SmartDialog.dismiss();\n              },\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  void _showShutdownDialog() {\n    SmartDialog.show(\n      builder: (BuildContext dialogContext) {\n        // Start the 10-second timer to auto close the dialog\n        _autoCloseDialogTimer?.cancel();\n        _autoCloseDialogTimer = Timer(const Duration(seconds: 10), () {\n          SmartDialog.dismiss(); // Close the dialog\n          _executeShutdown();\n        });\n        return AlertDialog(\n          title: const Text('定时关闭'),\n          content: const Text('将在10秒后执行，是否需要取消？'),\n          actions: <Widget>[\n            TextButton(\n              child: const Text('取消关闭'),\n              onPressed: () {\n                _autoCloseDialogTimer?.cancel(); // Cancel the auto-close timer\n                cancelShutdownTimer(); // Cancel the shutdown timer\n                SmartDialog.dismiss(); // Close the dialog\n              },\n            ),\n          ],\n        );\n      },\n    ).then((_) {\n      // Cleanup when the dialog is dismissed\n      _autoCloseDialogTimer?.cancel();\n    });\n  }\n\n  void _shutdownDecider() {\n    if (exitApp && !waitForPlayingCompleted) {\n      _showShutdownDialog();\n      return;\n    }\n    PlPlayerController plPlayerController =\n        PlPlayerController(videoType: 'none');\n    if (!exitApp && !waitForPlayingCompleted) {\n      if (!plPlayerController.playerStatus.playing) {\n        //仅提示用户\n        _showTimeUpButPauseDialog();\n      } else {\n        _showShutdownDialog();\n      }\n      return;\n    }\n    //waitForPlayingCompleted\n    if (!plPlayerController.playerStatus.playing) {\n      _showShutdownDialog();\n      return;\n    }\n    SmartDialog.showToast(\"定时关闭时间已到，等待当前视频播放完成\");\n    //监听播放完成\n    //该方法依赖耦合实现，不够优雅\n    isWaiting = true;\n  }\n\n  void handleWaitingFinished() {\n    if (isWaiting) {\n      _showShutdownDialog();\n      isWaiting = false;\n    }\n  }\n\n  void _executeShutdown() {\n    if (exitApp) {\n      //退出app\n      exit(0);\n    } else {\n      //暂停播放\n      PlPlayerController plPlayerController =\n          PlPlayerController(videoType: 'none');\n      if (plPlayerController.playerStatus.playing) {\n        plPlayerController.pause();\n        waitForPlayingCompleted = true;\n        SmartDialog.showToast(\"已暂停播放\");\n      } else {\n        SmartDialog.showToast(\"当前未播放\");\n      }\n    }\n  }\n\n  void cancelShutdownTimer() {\n    isWaiting = false;\n    _shutdownTimer?.cancel();\n  }\n}\n\nfinal shutdownTimerService = ShutdownTimerService();\n"
  },
  {
    "path": "lib/utils/app_scheme.dart",
    "content": "import 'package:appscheme/appscheme.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/utils/route_push.dart';\nimport '../http/search.dart';\nimport 'id_utils.dart';\nimport 'url_utils.dart';\nimport 'utils.dart';\n\nclass PiliSchame {\n  static AppScheme appScheme = AppSchemeImpl.getInstance()!;\n  static Future<void> init() async {\n    ///\n    final SchemeEntity? value = await appScheme.getInitScheme();\n    if (value != null) {\n      _routePush(value);\n    }\n\n    /// 完整链接进入 b23.无效\n    appScheme.getLatestScheme().then((SchemeEntity? value) {\n      if (value != null) {\n        _routePush(value);\n      }\n    });\n\n    /// 注册从外部打开的Scheme监听信息 #\n    appScheme.registerSchemeListener().listen((SchemeEntity? event) {\n      if (event != null) {\n        _routePush(event);\n      }\n    });\n  }\n\n  /// 路由跳转\n  static void _routePush(value) async {\n    final String scheme = value.scheme;\n    final String host = value.host;\n    final String path = value.path;\n    if (scheme == 'bilibili') {\n      switch (host) {\n        case 'root':\n          Navigator.popUntil(\n              Get.context!, (Route<dynamic> route) => route.isFirst);\n          break;\n        case 'space':\n          final String mid = path.split('/').last;\n          Get.toNamed<dynamic>(\n            '/member?mid=$mid',\n            arguments: <String, dynamic>{'face': null},\n          );\n          break;\n        case 'video':\n          String pathQuery = path.split('/').last;\n          final numericRegex = RegExp(r'^[0-9]+$');\n          if (numericRegex.hasMatch(pathQuery)) {\n            pathQuery = 'AV$pathQuery';\n          }\n          Map map = IdUtils.matchAvorBv(input: pathQuery);\n          if (map.containsKey('AV')) {\n            _videoPush(map['AV'], null);\n          } else if (map.containsKey('BV')) {\n            _videoPush(null, map['BV']);\n          } else {\n            SmartDialog.showToast('投稿匹配失败');\n          }\n          break;\n        case 'live':\n          final String roomId = path.split('/').last;\n          Get.toNamed<dynamic>(\n            '/liveRoom?roomid=$roomId',\n            arguments: <String, String?>{'liveItem': null, 'heroTag': roomId},\n          );\n          break;\n        case 'bangumi':\n          if (path.startsWith('/season')) {\n            final String seasonId = path.split('/').last;\n            RoutePush.bangumiPush(int.parse(seasonId), null);\n          }\n          break;\n        case 'opus':\n          if (path.startsWith('/detail')) {\n            var opusId = path.split('/').last;\n            Get.toNamed('/opus', parameters: {\n              'title': '',\n              'id': opusId,\n              'articleType': 'opus',\n            });\n          }\n          break;\n        case 'search':\n          Get.toNamed('/searchResult', parameters: {'keyword': ''});\n          break;\n        case 'article':\n          final String id = path.split('/').last.split('?').first;\n          Get.toNamed(\n            '/read',\n            parameters: {\n              'title': 'cv$id',\n              'id': id,\n              'dynamicType': 'read',\n            },\n          );\n          break;\n        case 'pgc':\n          if (path.contains('ep')) {\n            final String lastPathSegment = path.split('/').last;\n            RoutePush.bangumiPush(\n                null, int.parse(lastPathSegment.split('?').first));\n          }\n          break;\n        default:\n          SmartDialog.showToast('未匹配地址，请联系开发者');\n          Clipboard.setData(ClipboardData(text: value.toJson().toString()));\n          break;\n      }\n    }\n    if (scheme == 'https') {\n      fullPathPush(value);\n    }\n  }\n\n  // 投稿跳转\n  static Future<void> _videoPush(int? aidVal, String? bvidVal) async {\n    SmartDialog.showLoading<dynamic>(msg: '获取中...');\n    try {\n      int? aid = aidVal;\n      String? bvid = bvidVal;\n      if (aidVal == null) {\n        aid = IdUtils.bv2av(bvidVal!);\n      }\n      if (bvidVal == null) {\n        bvid = IdUtils.av2bv(aidVal!);\n      }\n      final int cid = await SearchHttp.ab2c(bvid: bvidVal, aid: aidVal);\n      final String heroTag = Utils.makeHeroTag(aid);\n      SmartDialog.dismiss<dynamic>().then(\n        // ignore: always_specify_types\n        (e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',\n            arguments: <String, String?>{\n              'pic': '',\n              'heroTag': heroTag,\n            }),\n      );\n    } catch (e) {\n      SmartDialog.showToast('video获取失败: $e');\n    }\n  }\n\n  static Future<void> fullPathPush(SchemeEntity value) async {\n    // https://m.bilibili.com/bangumi/play/ss39708\n    // https | m.bilibili.com | /bangumi/play/ss39708\n    // final String scheme = value.scheme!;\n    final String host = value.host!;\n    final String? path = value.path;\n    Map<String, String>? query = value.query;\n    RegExp regExp = RegExp(r'^((www\\.)|(m\\.))?bilibili\\.com$');\n    if (regExp.hasMatch(host)) {\n      final String lastPathSegment = path!.split('/').last;\n      if (path.startsWith('/video')) {\n        Map matchRes = IdUtils.matchAvorBv(input: path);\n        if (matchRes.containsKey('AV')) {\n          _videoPush(matchRes['AV']! as int, null);\n        } else if (matchRes.containsKey('BV')) {\n          _videoPush(null, matchRes['BV'] as String);\n        } else {\n          SmartDialog.showToast('投稿匹配失败');\n        }\n      }\n      if (path.startsWith('/bangumi')) {\n        if (lastPathSegment.contains('ss')) {\n          RoutePush.bangumiPush(Utils.matchNum(lastPathSegment).first, null);\n        }\n        if (lastPathSegment.contains('ep')) {\n          RoutePush.bangumiPush(null, Utils.matchNum(lastPathSegment).first);\n        }\n      } else if (path.startsWith('/BV')) {\n        final String bvid = path.split('?').first.split('/').last;\n        _videoPush(null, bvid);\n      } else if (path.startsWith('/av')) {\n        _videoPush(Utils.matchNum(path.split('?').first).first, null);\n      }\n    } else if (host.contains('live')) {\n      int roomId = int.parse(path!.split('/').last);\n      Get.toNamed(\n        '/liveRoom?roomid=$roomId',\n        arguments: {'liveItem': null, 'heroTag': roomId.toString()},\n      );\n    } else if (host.contains('space')) {\n      var mid = path!.split('/').last;\n      Get.toNamed('/member?mid=$mid', arguments: {'face': ''});\n      return;\n    } else if (host == 'b23.tv') {\n      final String fullPath = 'https://$host$path';\n      final String redirectUrl = await UrlUtils.parseRedirectUrl(fullPath);\n      final String pathSegment = Uri.parse(redirectUrl).path;\n      final String lastPathSegment = pathSegment.split('/').last;\n      final RegExp avRegex = RegExp(r'^[aA][vV]\\d+', caseSensitive: false);\n      if (avRegex.hasMatch(lastPathSegment)) {\n        final Map<String, dynamic> map =\n            IdUtils.matchAvorBv(input: lastPathSegment);\n        if (map.containsKey('AV')) {\n          _videoPush(map['AV']! as int, null);\n        } else if (map.containsKey('BV')) {\n          _videoPush(null, map['BV'] as String);\n        } else {\n          SmartDialog.showToast('投稿匹配失败');\n        }\n      } else if (lastPathSegment.startsWith('ep')) {\n        _handleEpisodePath(lastPathSegment, redirectUrl);\n      } else if (lastPathSegment.startsWith('ss')) {\n        _handleSeasonPath(lastPathSegment, redirectUrl);\n      } else if (lastPathSegment.startsWith('BV')) {\n        UrlUtils.matchUrlPush(\n          lastPathSegment,\n          '',\n          redirectUrl,\n        );\n      } else {\n        Get.toNamed(\n          '/webview',\n          parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''},\n        );\n      }\n    } else if (path != null) {\n      final String area = path.split('/').last;\n      switch (area) {\n        case 'bangumi':\n          print('番剧');\n          if (area.startsWith('ep')) {\n            RoutePush.bangumiPush(null, Utils.matchNum(area).first);\n          } else if (area.startsWith('ss')) {\n            RoutePush.bangumiPush(Utils.matchNum(area).first, null);\n          }\n          break;\n        case 'video':\n          print('投稿');\n          final Map<String, dynamic> map = IdUtils.matchAvorBv(input: path);\n          if (map.containsKey('AV')) {\n            _videoPush(map['AV']! as int, null);\n          } else if (map.containsKey('BV')) {\n            _videoPush(null, map['BV'] as String);\n          } else {\n            SmartDialog.showToast('投稿匹配失败');\n          }\n          break;\n        case 'read':\n          print('专栏');\n          String id = Utils.matchNum(query!['id']!).first.toString();\n          Get.toNamed('/read', parameters: {\n            'url': value.dataString!,\n            'title': '',\n            'id': id,\n            'articleType': 'read'\n          });\n          break;\n        case 'space':\n          print('个人空间');\n          Get.toNamed('/member?mid=$area', arguments: {'face': ''});\n          break;\n        default:\n          final Map<String, dynamic> map =\n              IdUtils.matchAvorBv(input: area.split('?').first);\n          if (map.containsKey('AV')) {\n            _videoPush(map['AV']! as int, null);\n          } else if (map.containsKey('BV')) {\n            _videoPush(null, map['BV'] as String);\n          } else {\n            Get.toNamed(\n              '/webview',\n              parameters: {\n                'url': value.dataString ?? \"\",\n                'type': 'url',\n                'pageTitle': ''\n              },\n            );\n          }\n          break;\n      }\n    }\n  }\n\n  static void _handleEpisodePath(String lastPathSegment, String redirectUrl) {\n    final String seasonId = _extractIdFromPath(lastPathSegment);\n    RoutePush.bangumiPush(null, Utils.matchNum(seasonId).first);\n  }\n\n  static void _handleSeasonPath(String lastPathSegment, String redirectUrl) {\n    final String seasonId = _extractIdFromPath(lastPathSegment);\n    RoutePush.bangumiPush(Utils.matchNum(seasonId).first, null);\n  }\n\n  static String _extractIdFromPath(String lastPathSegment) {\n    return lastPathSegment.split('/').last;\n  }\n}\n"
  },
  {
    "path": "lib/utils/binary_writer.dart",
    "content": "import 'dart:typed_data';\n\nclass BinaryWriter {\n  List<int> buffer;\n  int position = 0;\n  BinaryWriter(this.buffer);\n  int get length => buffer.length;\n\n  void writeBytes(List<int> list) {\n    buffer.addAll(list);\n    position += list.length;\n  }\n\n  void writeInt(int value, int len, {Endian endian = Endian.big}) {\n    var bytes = _createByteData(len);\n    switch (len) {\n      case 1:\n        bytes.setUint8(0, value.toUnsigned(8));\n        break;\n      case 2:\n        bytes.setInt16(0, value, endian);\n        break;\n      case 4:\n        bytes.setInt32(0, value, endian);\n        break;\n      case 8:\n        bytes.setInt64(0, value, endian);\n        break;\n      default:\n        throw ArgumentError('Invalid length for writeInt: $len');\n    }\n    _addBytesToBuffer(bytes, len);\n  }\n\n  void writeDouble(double value, int len, {Endian endian = Endian.big}) {\n    var bytes = _createByteData(len);\n    switch (len) {\n      case 4:\n        bytes.setFloat32(0, value, endian);\n        break;\n      case 8:\n        bytes.setFloat64(0, value, endian);\n        break;\n      default:\n        throw ArgumentError('Invalid length for writeDouble: $len');\n    }\n    _addBytesToBuffer(bytes, len);\n  }\n\n  ByteData _createByteData(int len) {\n    var b = Uint8List(len).buffer;\n    return ByteData.view(b);\n  }\n\n  void _addBytesToBuffer(ByteData bytes, int len) {\n    buffer.addAll(bytes.buffer.asUint8List());\n    position += len;\n  }\n}\n\nclass BinaryReader {\n  Uint8List buffer;\n  int position = 0;\n  BinaryReader(this.buffer);\n  int get length => buffer.length;\n\n  int read() {\n    return buffer[position++];\n  }\n\n  int readInt(int len, {Endian endian = Endian.big}) {\n    var bytes = _getBytes(len);\n    var data = ByteData.view(bytes.buffer);\n    switch (len) {\n      case 1:\n        return data.getUint8(0);\n      case 2:\n        return data.getInt16(0, endian);\n      case 4:\n        return data.getInt32(0, endian);\n      case 8:\n        return data.getInt64(0, endian);\n      default:\n        throw ArgumentError('Invalid length for readInt: $len');\n    }\n  }\n\n  int readByte({Endian endian = Endian.big}) => readInt(1, endian: endian);\n  int readShort({Endian endian = Endian.big}) => readInt(2, endian: endian);\n  int readInt32({Endian endian = Endian.big}) => readInt(4, endian: endian);\n  int readLong({Endian endian = Endian.big}) => readInt(8, endian: endian);\n\n  Uint8List readBytes(int len) {\n    var bytes = _getBytes(len);\n    return bytes;\n  }\n\n  double readFloat(int len, {Endian endian = Endian.big}) {\n    var bytes = _getBytes(len);\n    var data = ByteData.view(bytes.buffer);\n    switch (len) {\n      case 4:\n        return data.getFloat32(0, endian);\n      case 8:\n        return data.getFloat64(0, endian);\n      default:\n        throw ArgumentError('Invalid length for readFloat: $len');\n    }\n  }\n\n  Uint8List _getBytes(int len) {\n    var bytes =\n        Uint8List.fromList(buffer.getRange(position, position + len).toList());\n    position += len;\n    return bytes;\n  }\n}\n"
  },
  {
    "path": "lib/utils/cache_manage.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\n\nclass CacheManage {\n  CacheManage._internal();\n\n  static final CacheManage cacheManage = CacheManage._internal();\n\n  factory CacheManage() => cacheManage;\n\n  // 获取缓存目录\n  Future<String> loadApplicationCache() async {\n    /// clear all of image in memory\n    // clearMemoryImageCache();\n    /// get ImageCache\n    // var res = getMemoryImageCache();\n\n    // 缓存大小\n    double cacheSize = 0;\n    // cached_network_image directory\n    Directory tempDirectory = await getTemporaryDirectory();\n    // get_storage directory\n    Directory docDirectory = await getApplicationDocumentsDirectory();\n\n    // 获取缓存大小\n    if (tempDirectory.existsSync()) {\n      double value = await getTotalSizeOfFilesInDir(tempDirectory);\n      cacheSize += value;\n    }\n\n    /// 获取缓存大小 dioCache\n    if (docDirectory.existsSync()) {\n      double value = 0;\n      String dioCacheFileName =\n          '${docDirectory.path}${Platform.pathSeparator}DioCache.db';\n      var dioCacheFile = File(dioCacheFileName);\n      if (dioCacheFile.existsSync()) {\n        value = await getTotalSizeOfFilesInDir(dioCacheFile);\n      }\n      cacheSize += value;\n    }\n\n    return formatSize(cacheSize);\n  }\n\n  // 循环计算文件的大小（递归）\n  Future<double> getTotalSizeOfFilesInDir(final FileSystemEntity file) async {\n    if (file is File) {\n      int length = await file.length();\n      return double.parse(length.toString());\n    }\n    if (file is Directory) {\n      final List<FileSystemEntity> children = file.listSync();\n      double total = 0;\n      for (final FileSystemEntity child in children) {\n        total += await getTotalSizeOfFilesInDir(child);\n      }\n      return total;\n    }\n    return 0;\n  }\n\n  // 缓存大小格式转换\n  String formatSize(double value) {\n    List<String> unitArr = ['B', 'K', 'M', 'G'];\n    int index = 0;\n    while (value > 1024) {\n      index++;\n      value = value / 1024;\n    }\n    String size = value.toStringAsFixed(2);\n    return size + unitArr[index];\n  }\n\n  // 清除缓存\n  Future<bool> clearCacheAll() async {\n    bool cleanStatus = await SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: const Text('提示'),\n          content: const Text('该操作将清除图片及网络请求缓存数据，确认清除？'),\n          actions: [\n            TextButton(\n              onPressed: (() => {SmartDialog.dismiss()}),\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                SmartDialog.dismiss();\n                SmartDialog.showLoading(msg: '正在清除...');\n                try {\n                  // 清除缓存 图片缓存\n                  await clearLibraryCache();\n                  SmartDialog.dismiss().then((res) {\n                    SmartDialog.showToast('清除完成');\n                  });\n                } catch (err) {\n                  SmartDialog.dismiss();\n                  SmartDialog.showToast(err.toString());\n                }\n              },\n              child: const Text('确认'),\n            )\n          ],\n        );\n      },\n    ).then((res) {\n      return true;\n    });\n    return cleanStatus;\n  }\n\n  /// 清除 Documents 目录下的 DioCache.db\n  Future clearApplicationCache() async {\n    Directory directory = await getApplicationDocumentsDirectory();\n    if (directory.existsSync()) {\n      String dioCacheFileName =\n          '${directory.path}${Platform.pathSeparator}DioCache.db';\n      var dioCacheFile = File(dioCacheFileName);\n      if (dioCacheFile.existsSync()) {\n        dioCacheFile.delete();\n      }\n    }\n  }\n\n  // 清除 Library/Caches 目录及文件缓存\n  Future clearLibraryCache() async {\n    var appDocDir = await getTemporaryDirectory();\n    if (appDocDir.existsSync()) {\n      await appDocDir.delete(recursive: true);\n    }\n  }\n\n  /// 递归方式删除目录及文件\n  Future deleteDirectory(FileSystemEntity file) async {\n    if (file is Directory) {\n      final List<FileSystemEntity> children = file.listSync();\n      for (final FileSystemEntity child in children) {\n        await deleteDirectory(child);\n      }\n    }\n    await file.delete();\n  }\n}\n"
  },
  {
    "path": "lib/utils/cookie.dart",
    "content": "import 'package:pilipala/http/constants.dart';\nimport 'package:pilipala/http/init.dart';\nimport 'package:webview_cookie_manager/webview_cookie_manager.dart';\n\nclass SetCookie {\n  static onSet() async {\n    var cookies = await WebviewCookieManager().getCookies(HttpString.baseUrl);\n    await Request.cookieManager.cookieJar\n        .saveFromResponse(Uri.parse(HttpString.baseUrl), cookies);\n    var cookieString =\n        cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');\n    Request.dio.options.headers['cookie'] = cookieString;\n\n    cookies = await WebviewCookieManager().getCookies(HttpString.apiBaseUrl);\n    await Request.cookieManager.cookieJar\n        .saveFromResponse(Uri.parse(HttpString.apiBaseUrl), cookies);\n\n    cookies = await WebviewCookieManager().getCookies(HttpString.tUrl);\n    await Request.cookieManager.cookieJar\n        .saveFromResponse(Uri.parse(HttpString.tUrl), cookies);\n  }\n}\n"
  },
  {
    "path": "lib/utils/danmaku.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ns_danmaku/ns_danmaku.dart';\n\nclass DmUtils {\n  static Color decimalToColor(int decimalColor) {\n    // 16777215 表示白色\n    int red = (decimalColor >> 16) & 0xFF;\n    int green = (decimalColor >> 8) & 0xFF;\n    int blue = decimalColor & 0xFF;\n\n    return Color.fromARGB(255, red, green, blue);\n  }\n\n  static DanmakuItemType getPosition(int mode) {\n    DanmakuItemType type = DanmakuItemType.scroll;\n    if (mode >= 1 && mode <= 3) {\n      type = DanmakuItemType.scroll;\n    } else if (mode == 4) {\n      type = DanmakuItemType.bottom;\n    } else if (mode == 5) {\n      type = DanmakuItemType.top;\n    }\n    return type;\n  }\n}\n"
  },
  {
    "path": "lib/utils/data.dart",
    "content": "import 'package:hive/hive.dart';\nimport 'package:pilipala/http/user.dart';\n\nimport 'storage.dart';\n\nclass Data {\n  static Future init() async {\n    await historyStatus();\n  }\n\n  static Future historyStatus() async {\n    Box localCache = GStrorage.localCache;\n    Box userInfoCache = GStrorage.userInfo;\n    if (userInfoCache.get('userInfoCache') == null) {\n      return;\n    }\n    var res = await UserHttp.historyStatus();\n    localCache.put(LocalCacheKey.historyPause, res.data['data']);\n  }\n}\n"
  },
  {
    "path": "lib/utils/download.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:dio/dio.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:permission_handler/permission_handler.dart';\nimport 'package:saver_gallery/saver_gallery.dart';\n\nclass DownloadUtils {\n  // 获取存储权限\n  static Future<bool> requestStoragePer() async {\n    await Permission.storage.request();\n    PermissionStatus status = await Permission.storage.status;\n    if (status == PermissionStatus.denied ||\n        status == PermissionStatus.permanentlyDenied) {\n      await permissionDialog('提示', '存储权限未授权');\n      return false;\n    } else {\n      return true;\n    }\n  }\n\n  // 获取相册权限\n  static Future<bool> requestPhotoPer() async {\n    await Permission.photos.request();\n    PermissionStatus status = await Permission.photos.status;\n    if (status == PermissionStatus.denied ||\n        status == PermissionStatus.permanentlyDenied) {\n      await permissionDialog('提示', '相册权限未授权');\n      return false;\n    } else {\n      return true;\n    }\n  }\n\n  static Future<bool> downloadImg(String imgUrl,\n      {String imgType = 'cover'}) async {\n    try {\n      if (Platform.isAndroid) {\n        final androidInfo = await DeviceInfoPlugin().androidInfo;\n        if (androidInfo.version.sdkInt <= 32) {\n          if (!await requestStoragePer()) {\n            return false;\n          }\n        } else {\n          if (!await requestPhotoPer()) {\n            return false;\n          }\n        }\n      }\n\n      SmartDialog.showLoading(msg: '保存中');\n      var response = await Dio()\n          .get(imgUrl, options: Options(responseType: ResponseType.bytes));\n      final String imgSuffix = imgUrl.split('.').last;\n      String picName =\n          \"plpl_${imgType}_${DateTime.now().toString().replaceAll(RegExp(r'[- :]'), '').split('.').first}\";\n      final SaveResult result = await SaverGallery.saveImage(\n        Uint8List.fromList(response.data),\n        name: '$picName.$imgSuffix',\n        // 保存到 PiliPala文件夹\n        androidRelativePath: \"Pictures/PiliPala\",\n        androidExistNotSave: false,\n      );\n      SmartDialog.dismiss();\n      if (result.isSuccess) {\n        SmartDialog.showToast('「${'$picName.$imgSuffix'}」已保存 ');\n        return true;\n      } else {\n        await permissionDialog('保存失败', '相册权限未授权');\n        return false;\n      }\n    } catch (err) {\n      SmartDialog.dismiss();\n      SmartDialog.showToast(err.toString());\n      return false;\n    }\n  }\n\n  static Future permissionDialog(String title, String content,\n      {Function? onGranted}) async {\n    await SmartDialog.show(\n      useSystem: true,\n      animationType: SmartAnimationType.centerFade_otherSlide,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: Text(title),\n          content: Text(content),\n          actions: [\n            TextButton(\n              onPressed: () async {\n                openAppSettings();\n              },\n              child: const Text('去授权'),\n            )\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/utils/drawer.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\n\nclass DrawerUtils {\n  static void showRightDialog({\n    required Widget child,\n    double width = 400,\n    bool useSystem = false,\n  }) {\n    SmartDialog.show(\n      alignment: Alignment.topRight,\n      animationBuilder: (controller, child, animationParam) {\n        return SlideTransition(\n          position: Tween<Offset>(\n            begin: const Offset(1, 0),\n            end: Offset.zero,\n          ).animate(controller.view),\n          child: child,\n        );\n      },\n      useSystem: useSystem,\n      maskColor: Colors.black.withOpacity(0.5),\n      animationTime: const Duration(milliseconds: 200),\n      builder: (context) => Container(\n        width: width,\n        color: Theme.of(context).scaffoldBackgroundColor,\n        child: SafeArea(\n          left: false,\n          right: false,\n          bottom: false,\n          child: MediaQuery(\n            data: const MediaQueryData(padding: EdgeInsets.zero),\n            child: child,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/utils/em.dart",
    "content": "class Em {\n  static regCate(String origin) {\n    String str = origin;\n    RegExp exp = RegExp('<[^>]*>([^<]*)</[^>]*>');\n    Iterable<Match> matches = exp.allMatches(origin);\n    for (Match match in matches) {\n      str = match.group(1)!;\n    }\n    return str;\n  }\n\n  static regTitle(String origin) {\n    RegExp exp = RegExp('<[^>]*>([^<]*)</[^>]*>');\n    List res = [];\n    origin.splitMapJoin(exp, onMatch: (Match match) {\n      String matchStr = match[0]!;\n      Map map = {'type': 'em', 'text': regCate(matchStr)};\n      res.add(map);\n      return regCate(matchStr);\n    }, onNonMatch: (String str) {\n      if (str != '') {\n        str = decodeHtmlEntities(str);\n        Map map = {'type': 'text', 'text': str};\n        res.add(map);\n      }\n      return str;\n    });\n    return res;\n  }\n\n  static String decodeHtmlEntities(String title) {\n    return title\n        .replaceAll('&lt;', '<')\n        .replaceAll('&gt;', '>')\n        .replaceAll('&#34;', '\"')\n        .replaceAll('&#39;', \"'\")\n        .replaceAll('&quot;', '\"')\n        .replaceAll('&apos;', \"'\")\n        .replaceAll('&nbsp;', \" \")\n        .replaceAll('&amp;', \"&\")\n        .replaceAll('&#x27;', \"'\");\n  }\n}\n"
  },
  {
    "path": "lib/utils/event_bus.dart",
    "content": "// 订阅者回调签名\ntypedef void EventCallback(arg);\n\nclass EventBus {\n  // 私有构造函数\n  EventBus._internal();\n\n  // 保存单例\n  static final EventBus _singleton = EventBus._internal();\n\n  // 工厂构造函数\n  factory EventBus() => _singleton;\n\n  // 保存事件订阅者队列，key:事件名(id)，value: 对应事件的订阅者队列\n  final _emap = <dynamic, List<EventCallback>>{};\n\n  // 添加订阅者\n  void on(eventName, EventCallback f) {\n    _emap[eventName] ??= <EventCallback>[];\n    _emap[eventName]!.add(f);\n  }\n\n  // 移除订阅者\n  void off(eventName, [EventCallback? f]) {\n    var list = _emap[eventName];\n    if (eventName == null || list == null) return;\n    if (f == null) {\n      _emap[eventName] = [];\n    } else {\n      list.remove(f);\n    }\n  }\n\n  // 触发事件，事件触发后该事件所有订阅者会被调用\n  void emit(eventName, [arg]) {\n    var list = _emap[eventName];\n    if (list == null) return;\n    List<EventCallback> tempList = List.from(list);\n    for (var callback in tempList) {\n      callback(arg);\n    }\n  }\n\n  // 获取订阅者数量\n  int getSubscriberCount(eventName) {\n    var list = _emap[eventName];\n    return list?.length ?? 0;\n  }\n}\n\nclass EventName {\n  static const String loginEvent = 'loginEvent';\n}\n"
  },
  {
    "path": "lib/utils/extension.dart",
    "content": "import 'package:flutter/material.dart';\n\nextension ImageExtension on num {\n  int cacheSize(BuildContext context) {\n    return (this * MediaQuery.of(context).devicePixelRatio).round();\n  }\n}\n"
  },
  {
    "path": "lib/utils/feed_back.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:hive/hive.dart';\nimport 'storage.dart';\n\nBox<dynamic> setting = GStrorage.setting;\nvoid feedBack() {\n  // 设置中是否开启\n  final bool enable =\n      setting.get(SettingBoxKey.feedBackEnable, defaultValue: false) as bool;\n  if (enable) {\n    HapticFeedback.lightImpact();\n  }\n}\n"
  },
  {
    "path": "lib/utils/global_data_cache.dart",
    "content": "import 'package:hive/hive.dart';\nimport 'package:pilipala/models/user/info.dart';\nimport 'package:pilipala/plugin/pl_player/models/play_repeat.dart';\nimport 'package:pilipala/plugin/pl_player/models/play_speed.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport '../models/common/index.dart';\n\nBox setting = GStrorage.setting;\nBox localCache = GStrorage.localCache;\nBox videoStorage = GStrorage.video;\nBox userInfoCache = GStrorage.userInfo;\n\nclass GlobalDataCache {\n  late int imgQuality;\n  late FullScreenGestureMode fullScreenGestureMode;\n  late bool enablePlayerControlAnimation;\n  late List<String> actionTypeSort;\n\n  /// 播放器相关\n  // 弹幕开关\n  late bool isOpenDanmu;\n  // 弹幕屏蔽类型\n  late List<dynamic> blockTypes;\n  // 弹幕展示区域\n  late double showArea;\n  // 弹幕透明度\n  late double opacityVal;\n  // 弹幕字体大小\n  late double fontSizeVal;\n  // 弹幕显示时间\n  late double danmakuDurationVal;\n  // 弹幕描边宽度\n  late double strokeWidth;\n  // 播放器循环模式\n  late PlayRepeat playRepeat;\n  // 播放器默认播放速度\n  late double playbackSpeed;\n  // 播放器自动长按速度\n  late bool enableAutoLongPressSpeed;\n  // 播放器长按速度\n  late double longPressSpeed;\n  // 播放器速度列表\n  late List<double> speedsList;\n  // 用户信息\n  UserInfoData? userInfo;\n\n  // 私有构造函数\n  GlobalDataCache._();\n\n  // 单例实例\n  static final GlobalDataCache _instance = GlobalDataCache._();\n\n  // 获取全局实例\n  factory GlobalDataCache() => _instance;\n\n  // 异步初始化方法\n  Future<void> initialize() async {\n    imgQuality = await setting.get(SettingBoxKey.defaultPicQa,\n        defaultValue: 10); // 设置全局变量\n    fullScreenGestureMode = FullScreenGestureMode.values[setting.get(\n        SettingBoxKey.fullScreenGestureMode,\n        defaultValue: FullScreenGestureMode.values.last.index) as int];\n    enablePlayerControlAnimation = setting\n        .get(SettingBoxKey.enablePlayerControlAnimation, defaultValue: true);\n    actionTypeSort = await setting.get(SettingBoxKey.actionTypeSort,\n        defaultValue: ['like', 'coin', 'collect', 'watchLater', 'share']);\n\n    isOpenDanmu =\n        await setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);\n    blockTypes =\n        await localCache.get(LocalCacheKey.danmakuBlockType, defaultValue: []);\n    showArea =\n        await localCache.get(LocalCacheKey.danmakuShowArea, defaultValue: 0.5);\n    opacityVal =\n        await localCache.get(LocalCacheKey.danmakuOpacity, defaultValue: 1.0);\n    fontSizeVal =\n        await localCache.get(LocalCacheKey.danmakuFontScale, defaultValue: 1.0);\n    danmakuDurationVal =\n        await localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0);\n    strokeWidth =\n        await localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);\n\n    var defaultPlayRepeat = await videoStorage.get(VideoBoxKey.playRepeat,\n        defaultValue: PlayRepeat.pause.value);\n    playRepeat = PlayRepeat.values\n        .toList()\n        .firstWhere((e) => e.value == defaultPlayRepeat);\n    playbackSpeed =\n        await videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);\n    enableAutoLongPressSpeed = await setting\n        .get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);\n    if (!enableAutoLongPressSpeed) {\n      longPressSpeed = await videoStorage.get(VideoBoxKey.longPressSpeedDefault,\n          defaultValue: 2.0);\n    } else {\n      longPressSpeed = 2.0;\n    }\n    speedsList = List<double>.from(await videoStorage\n        .get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));\n    final List<double> playSpeedSystem = await videoStorage\n        .get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);\n    speedsList.addAll(playSpeedSystem);\n\n    userInfo = userInfoCache.get('userInfoCache');\n  }\n}\n"
  },
  {
    "path": "lib/utils/highlight.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:re_highlight/languages/all.dart';\nimport 'package:re_highlight/re_highlight.dart';\nimport 'package:re_highlight/styles/all.dart';\n\nTextSpan? highlightExistingText(String text, List<String> languages) {\n  final Highlight highlight = Highlight();\n  highlight.registerLanguages(builtinAllLanguages);\n  final HighlightResult result = highlight.highlightAuto(text, languages);\n  final TextSpanRenderer renderer =\n      TextSpanRenderer(const TextStyle(), builtinAllThemes['github']!);\n  result.render(renderer);\n  return renderer.span;\n}\n"
  },
  {
    "path": "lib/utils/id_utils.dart",
    "content": "// ignore_for_file: constant_identifier_names, non_constant_identifier_names\n\nimport 'dart:convert';\n\nclass IdUtils {\n  static final XOR_CODE = BigInt.parse('23442827791579');\n  static final MASK_CODE = BigInt.parse('2251799813685247');\n  static final MAX_AID = BigInt.one << (BigInt.from(51)).toInt();\n  static final BASE = BigInt.from(58);\n\n  static const data =\n      'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf';\n\n  /// av转bv\n  static String av2bv(int aid) {\n    List<String> bytes = [\n      'B',\n      'V',\n      '1',\n      '0',\n      '0',\n      '0',\n      '0',\n      '0',\n      '0',\n      '0',\n      '0',\n      '0'\n    ];\n    int bvIndex = bytes.length - 1;\n    BigInt tmp = (MAX_AID | BigInt.from(aid)) ^ XOR_CODE;\n    while (tmp > BigInt.zero) {\n      bytes[bvIndex] = data[(tmp % BASE).toInt()];\n      tmp = tmp ~/ BASE;\n      bvIndex -= 1;\n    }\n    String tmpSwap = bytes[3];\n    bytes[3] = bytes[9];\n    bytes[9] = tmpSwap;\n\n    tmpSwap = bytes[4];\n    bytes[4] = bytes[7];\n    bytes[7] = tmpSwap;\n\n    return bytes.join();\n  }\n\n  /// bv转av\n  static int bv2av(String bvid) {\n    List<String> bvidArr = bvid.split('');\n    final tmpValue = bvidArr[3];\n    bvidArr[3] = bvidArr[9];\n    bvidArr[9] = tmpValue;\n\n    final tmpValue2 = bvidArr[4];\n    bvidArr[4] = bvidArr[7];\n    bvidArr[7] = tmpValue2;\n\n    bvidArr.removeRange(0, 3);\n    BigInt tmp = bvidArr.fold(BigInt.zero,\n        (pre, bvidChar) => pre * BASE + BigInt.from(data.indexOf(bvidChar)));\n    return ((tmp & MASK_CODE) ^ XOR_CODE).toInt();\n  }\n\n  // 匹配\n  static Map<String, dynamic> matchAvorBv({String? input}) {\n    final Map<String, dynamic> result = {};\n    if (input == null || input.isEmpty) {\n      return result;\n    }\n    final RegExp bvRegex =\n        RegExp(r'[bB][vV][0-9A-Za-z]{10}', caseSensitive: false);\n    final RegExp avRegex = RegExp(r'[aA][vV]\\d+', caseSensitive: false);\n\n    final Iterable<Match> bvMatches = bvRegex.allMatches(input);\n    final Iterable<Match> avMatches = avRegex.allMatches(input);\n\n    final List<String> bvs =\n        bvMatches.map((Match match) => match.group(0)!).toList();\n    final List<String> avs =\n        avMatches.map((Match match) => match.group(0)!).toList();\n\n    if (bvs.isNotEmpty) {\n      result['BV'] = bvs[0].substring(0, 2).toUpperCase() + bvs[0].substring(2);\n    }\n    if (avs.isNotEmpty) {\n      result['AV'] = int.parse(avs[0].substring(2));\n    }\n    return result;\n  }\n\n  // eid生成\n  static String? genAuroraEid(int uid) {\n    if (uid == 0) {\n      return null;\n    }\n    String uidString = uid.toString();\n    List<int> resultBytes = List.generate(\n      uidString.length,\n      (i) => uidString.codeUnitAt(i) ^ \"ad1va46a7lza\".codeUnitAt(i % 12),\n    );\n    String auroraEid = base64Url.encode(resultBytes);\n    auroraEid = auroraEid.replaceAll(RegExp(r'=*$', multiLine: true), '');\n    return auroraEid;\n  }\n}\n"
  },
  {
    "path": "lib/utils/image_save.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:pilipala/common/constants.dart';\nimport 'package:pilipala/common/widgets/network_img_layer.dart';\nimport 'package:pilipala/utils/download.dart';\n\nFuture imageSaveDialog(context, videoItem, closeFn) {\n  final double imgWidth =\n      MediaQuery.sizeOf(context).width - StyleString.safeSpace * 2;\n  return SmartDialog.show(\n    animationType: SmartAnimationType.centerScale_otherSlide,\n    builder: (context) => Container(\n      margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surface,\n        borderRadius: BorderRadius.circular(10.0),\n      ),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Stack(\n            children: [\n              NetworkImgLayer(\n                width: imgWidth,\n                height: imgWidth / StyleString.aspectRatio,\n                src: videoItem.pic! as String,\n                quality: 100,\n              ),\n              Positioned(\n                right: 8,\n                top: 8,\n                child: Container(\n                  width: 30,\n                  height: 30,\n                  decoration: BoxDecoration(\n                      color: Colors.black.withOpacity(0.3),\n                      borderRadius:\n                          const BorderRadius.all(Radius.circular(20))),\n                  child: IconButton(\n                    style: ButtonStyle(\n                      padding: MaterialStateProperty.all(EdgeInsets.zero),\n                    ),\n                    onPressed: () => closeFn!(),\n                    icon: const Icon(\n                      Icons.close,\n                      size: 18,\n                      color: Colors.white,\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ),\n          Padding(\n            padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),\n            child: Row(\n              children: [\n                Expanded(\n                  child: SelectableText(\n                    videoItem.title! as String,\n                    style: Theme.of(context).textTheme.titleSmall,\n                  ),\n                ),\n                const SizedBox(width: 4),\n                IconButton(\n                  tooltip: '保存封面图',\n                  onPressed: () async {\n                    bool saveStatus = await DownloadUtils.downloadImg(\n                      videoItem.pic != null\n                          ? videoItem.pic as String\n                          : videoItem.cover as String,\n                    );\n                    // 保存成功，自动关闭弹窗\n                    if (saveStatus) {\n                      closeFn?.call();\n                    }\n                  },\n                  icon: const Icon(Icons.download, size: 20),\n                )\n              ],\n            ),\n          ),\n        ],\n      ),\n    ),\n  );\n}\n"
  },
  {
    "path": "lib/utils/live.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\nimport 'package:brotli/brotli.dart';\nimport 'package:pilipala/models/live/message.dart';\nimport 'package:pilipala/utils/binary_writer.dart';\n\nclass LiveUtils {\n  static List<int> encodeData(String msg, int action) {\n    var data = utf8.encode(msg);\n    //头部长度固定16\n    var length = data.length + 16;\n    var buffer = Uint8List(length);\n\n    var writer = BinaryWriter([]);\n\n    //数据包长度\n    writer.writeInt(buffer.length, 4);\n    //数据包头部长度,固定16\n    writer.writeInt(16, 2);\n\n    //协议版本，0=JSON,1=Int32,2=Buffer\n    writer.writeInt(0, 2);\n\n    //操作类型\n    writer.writeInt(action, 4);\n\n    //数据包头部长度,固定1\n\n    writer.writeInt(1, 4);\n\n    writer.writeBytes(data);\n\n    return writer.buffer;\n  }\n\n  static List<LiveMessageModel>? decodeMessage(List<int> data) {\n    try {\n      //操作类型。3=心跳回应，内容为房间人气值；5=通知，弹幕、广播等全部信息；8=进房回应，空\n      int operation = readInt(data, 8, 4);\n      //内容\n      var body = data.skip(16).toList();\n      if (operation == 3) {\n        var online = readInt(body, 0, 4);\n        final LiveMessageModel liveMsg = LiveMessageModel(\n          type: LiveMessageType.online,\n          userName: '',\n          message: '',\n          color: LiveMessageColor.white,\n          data: online,\n        );\n        return [liveMsg];\n      } else if (operation == 5) {\n        //协议版本。0为JSON，可以直接解析；1为房间人气值,Body为4位Int32；2为压缩过Buffer，需要解压再处理\n        int protocolVersion = readInt(data, 6, 2);\n        if (protocolVersion == 2) {\n          body = zlib.decode(body);\n        } else if (protocolVersion == 3) {\n          body = brotli.decode(body);\n        }\n\n        var text = utf8.decode(body, allowMalformed: true);\n\n        var group =\n            text.split(RegExp(r\"[\\x00-\\x1f]+\", unicode: true, multiLine: true));\n        List<LiveMessageModel> messages = [];\n        for (var item\n            in group.where((x) => x.length > 2 && x.startsWith('{'))) {\n          if (parseMessage(item) is LiveMessageModel) {\n            messages.add(parseMessage(item)!);\n          }\n        }\n        return messages;\n      }\n    } catch (e) {\n      print(e);\n    }\n    return null;\n  }\n\n  static LiveMessageModel? parseMessage(String jsonMessage) {\n    try {\n      var obj = json.decode(jsonMessage);\n      var cmd = obj[\"cmd\"].toString();\n      if (cmd.contains(\"DANMU_MSG\")) {\n        if (obj[\"info\"] != null && obj[\"info\"].length != 0) {\n          var message = obj[\"info\"][1].toString();\n          var color = asT<int?>(obj[\"info\"][0][3]) ?? 0;\n          if (obj[\"info\"][2] != null && obj[\"info\"][2].length != 0) {\n            var extra = obj[\"info\"][0][15]['extra'];\n            var user = obj[\"info\"][0][15]['user']['base'];\n            Map<String, dynamic> extraMap = jsonDecode(extra);\n            final int userId = obj[\"info\"][2][0];\n            final LiveMessageModel liveMsg = LiveMessageModel(\n              type: LiveMessageType.chat,\n              userName: user['name'],\n              message: message,\n              color: color == 0\n                  ? LiveMessageColor.white\n                  : LiveMessageColor.numberToColor(color),\n              face: user['face'],\n              uid: userId,\n              emots: extraMap['emots'],\n            );\n            return liveMsg;\n          }\n        }\n      } else if (cmd == \"SUPER_CHAT_MESSAGE\") {\n        if (obj[\"data\"] == null) {\n          return null;\n        }\n        final data = obj[\"data\"];\n        final userInfo = data[\"user_info\"];\n        final String backgroundBottomColor =\n            data[\"background_bottom_color\"].toString();\n        final String backgroundColor = data[\"background_color\"].toString();\n        final DateTime endTime =\n            DateTime.fromMillisecondsSinceEpoch(data[\"end_time\"] * 1000);\n        final String face = \"${userInfo[\"face\"]}@200w.jpg\";\n        final String message = data[\"message\"].toString();\n        final String price = data[\"price\"];\n        final DateTime startTime =\n            DateTime.fromMillisecondsSinceEpoch(data[\"start_time\"] * 1000);\n        final String userName = userInfo[\"uname\"].toString();\n\n        final LiveMessageModel liveMsg = LiveMessageModel(\n          type: LiveMessageType.superChat,\n          userName: \"SUPER_CHAT_MESSAGE\",\n          message: \"SUPER_CHAT_MESSAGE\",\n          color: LiveMessageColor.white,\n          data: {\n            \"backgroundBottomColor\": backgroundBottomColor,\n            \"backgroundColor\": backgroundColor,\n            \"endTime\": endTime,\n            \"face\": face,\n            \"message\": message,\n            \"price\": price,\n            \"startTime\": startTime,\n            \"userName\": userName,\n          },\n        );\n        return liveMsg;\n      } else if (cmd == 'INTERACT_WORD') {\n        if (obj[\"data\"] == null) {\n          return null;\n        }\n        final data = obj[\"data\"];\n        final String userName = data['uname'];\n        final int msgType = data['msg_type'];\n        final LiveMessageModel liveMsg = LiveMessageModel(\n          type: msgType == 1 ? LiveMessageType.join : LiveMessageType.follow,\n          userName: userName,\n          message: msgType == 1 ? '进入直播间' : '关注了主播',\n          color: LiveMessageColor.white,\n        );\n        return liveMsg;\n      }\n    } catch (e) {\n      print(e);\n    }\n    return null;\n  }\n\n  static T? asT<T>(dynamic value) {\n    if (value is T) {\n      return value;\n    }\n    return null;\n  }\n\n  static int readInt(List<int> buffer, int start, int len) {\n    var data = _getByteData(buffer, start, len);\n    return _readIntFromByteData(data, len);\n  }\n\n  static ByteData _getByteData(List<int> buffer, int start, int len) {\n    var bytes =\n        Uint8List.fromList(buffer.getRange(start, start + len).toList());\n    return ByteData.view(bytes.buffer);\n  }\n\n  static int _readIntFromByteData(ByteData data, int len) {\n    switch (len) {\n      case 1:\n        return data.getUint8(0);\n      case 2:\n        return data.getInt16(0, Endian.big);\n      case 4:\n        return data.getInt32(0, Endian.big);\n      case 8:\n        return data.getInt64(0, Endian.big);\n      default:\n        throw ArgumentError('Invalid length: $len');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/login.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:hive/hive.dart';\nimport 'package:pilipala/http/user.dart';\nimport 'package:pilipala/pages/dynamics/index.dart';\nimport 'package:pilipala/pages/home/index.dart';\nimport 'package:pilipala/pages/media/index.dart';\nimport 'package:pilipala/pages/mine/index.dart';\nimport 'package:pilipala/utils/cookie.dart';\nimport 'package:pilipala/utils/storage.dart';\nimport 'package:uuid/uuid.dart';\n\nclass LoginUtils {\n  static Future refreshLoginStatus(bool status) async {\n    try {\n      // 更改我的页面登录状态\n      await Get.find<MineController>().resetUserInfo();\n\n      // 更改主页登录状态\n      HomeController homeCtr = Get.find<HomeController>();\n      homeCtr.updateLoginStatus(status);\n\n      MineController mineCtr = Get.find<MineController>();\n      mineCtr.userLogin.value = status;\n\n      DynamicsController dynamicsCtr = Get.find<DynamicsController>();\n      dynamicsCtr.userLogin.value = status;\n\n      MediaController mediaCtr = Get.find<MediaController>();\n      mediaCtr.userLogin.value = status;\n    } catch (err) {\n      SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}');\n    }\n  }\n\n  static String buvid() {\n    var mac = <String>[];\n    var random = Random();\n\n    for (var i = 0; i < 6; i++) {\n      var min = 0;\n      var max = 0xff;\n      var num = (random.nextInt(max - min + 1) + min).toRadixString(16);\n      mac.add(num);\n    }\n\n    var md5Str = md5.convert(utf8.encode(mac.join(':'))).toString();\n    var md5Arr = md5Str.split('');\n    return 'XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5Str';\n  }\n\n  static String getUUID() {\n    return const Uuid().v4().replaceAll('-', '');\n  }\n\n  static String generateBuvid() {\n    String uuid = getUUID() + getUUID();\n    return 'XY${uuid.substring(0, 35).toUpperCase()}';\n  }\n\n  static confirmLogin(url, controller) async {\n    var content = '';\n    if (url != null) {\n      content = '${content + url}; \\n';\n    }\n    try {\n      await SetCookie.onSet();\n      final result = await UserHttp.userInfo();\n      if (result['status'] && result['data'].isLogin) {\n        SmartDialog.showToast('登录成功');\n        try {\n          Box userInfoCache = GStrorage.userInfo;\n          if (!userInfoCache.isOpen) {\n            userInfoCache = await Hive.openBox('userInfo');\n          }\n          await userInfoCache.put('userInfoCache', result['data']);\n\n          final HomeController homeCtr = Get.find<HomeController>();\n          homeCtr.updateLoginStatus(true);\n          homeCtr.userFace.value = result['data'].face;\n          final MediaController mediaCtr = Get.find<MediaController>();\n          mediaCtr.mid = result['data'].mid;\n          await LoginUtils.refreshLoginStatus(true);\n        } catch (err) {\n          SmartDialog.show(builder: (BuildContext context) {\n            return AlertDialog(\n              title: const Text('登录遇到问题'),\n              content: Text(err.toString()),\n              actions: [\n                TextButton(\n                  onPressed: controller != null\n                      ? () => controller.reload()\n                      : SmartDialog.dismiss,\n                  child: const Text('确认'),\n                )\n              ],\n            );\n          });\n        }\n        Get.back();\n      } else {\n        // 获取用户信息失败\n        SmartDialog.showToast(result['msg']);\n        Clipboard.setData(ClipboardData(text: result['msg']));\n      }\n    } catch (e) {\n      SmartDialog.showNotify(msg: e.toString(), notifyType: NotifyType.warning);\n      content = content + e.toString();\n      Clipboard.setData(ClipboardData(text: content));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/main_stream.dart",
    "content": "import 'dart:async';\nimport 'package:flutter/material.dart';\nimport 'package:easy_debounce/easy_throttle.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:get/get.dart';\n\nimport '../pages/home/index.dart';\nimport '../pages/main/index.dart';\n\nvoid handleScrollEvent(ScrollController scrollController) {\n  StreamController<bool> mainStream =\n      Get.find<MainController>().bottomBarStream;\n  StreamController<bool> searchBarStream =\n      Get.find<HomeController>().searchBarStream;\n  EasyThrottle.throttle(\n    'stream-throttler',\n    const Duration(milliseconds: 300),\n    () {\n      try {\n        final ScrollDirection direction =\n            scrollController.position.userScrollDirection;\n        if (direction == ScrollDirection.forward) {\n          mainStream.add(true);\n          searchBarStream.add(true);\n        } else if (direction == ScrollDirection.reverse) {\n          mainStream.add(false);\n          searchBarStream.add(false);\n        }\n      } catch (_) {}\n    },\n  );\n}\n"
  },
  {
    "path": "lib/utils/proxy.dart",
    "content": "import 'dart:io';\nimport 'package:system_proxy/system_proxy.dart';\n\nclass CustomProxy {\n  init() async {\n    Map<String, String>? proxy = await SystemProxy.getProxySettings();\n    if (proxy != null) {\n      HttpOverrides.global =\n          ProxiedHttpOverrides(proxy['host']!, proxy['port']!);\n    }\n  }\n}\n\nclass ProxiedHttpOverrides extends HttpOverrides {\n  final String _port;\n  final String _host;\n\n  ProxiedHttpOverrides(this._host, this._port);\n\n  @override\n  HttpClient createHttpClient(SecurityContext? context) {\n    return super.createHttpClient(context)\n      // set proxy\n      ..findProxy = (uri) {\n        return \"PROXY $_host:$_port;\";\n      };\n  }\n}\n"
  },
  {
    "path": "lib/utils/recommend_filter.dart",
    "content": "import 'dart:math';\n\nimport 'storage.dart';\n\nclass RecommendFilter {\n  // static late int filterUnfollowedRatio;\n  static late int minDurationForRcmd;\n  static late int minLikeRatioForRecommend;\n  static late bool exemptFilterForFollowed;\n  static late bool applyFilterToRelatedVideos;\n  RecommendFilter() {\n    update();\n  }\n\n  static void update() {\n    var setting = GStrorage.setting;\n    // filterUnfollowedRatio =\n    //     setting.get(SettingBoxKey.filterUnfollowedRatio, defaultValue: 0);\n    minDurationForRcmd =\n        setting.get(SettingBoxKey.minDurationForRcmd, defaultValue: 0);\n    minLikeRatioForRecommend =\n        setting.get(SettingBoxKey.minLikeRatioForRecommend, defaultValue: 0);\n    exemptFilterForFollowed =\n        setting.get(SettingBoxKey.exemptFilterForFollowed, defaultValue: true);\n    applyFilterToRelatedVideos = setting\n        .get(SettingBoxKey.applyFilterToRelatedVideos, defaultValue: true);\n  }\n\n  static bool filter(dynamic videoItem, {bool relatedVideos = false}) {\n    if (relatedVideos && !applyFilterToRelatedVideos) {\n      return false;\n    }\n    //由于相关视频中没有已关注标签，只能视为非关注视频\n    if (!relatedVideos &&\n        videoItem.isFollowed == 1 &&\n        exemptFilterForFollowed) {\n      return false;\n    }\n    if (videoItem.duration > 0 && videoItem.duration < minDurationForRcmd) {\n      return true;\n    }\n    if (videoItem.stat.view is int &&\n        videoItem.stat.view > -1 &&\n        videoItem.stat.like is int &&\n        videoItem.stat.like > -1 &&\n        videoItem.stat.like * 100 <\n            minLikeRatioForRecommend * videoItem.stat.view) {\n      return true;\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "lib/utils/route_push.dart",
    "content": "import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:get/get.dart';\nimport 'package:pilipala/http/search.dart';\nimport 'package:pilipala/models/bangumi/info.dart';\nimport 'package:pilipala/models/common/search_type.dart';\nimport 'package:pilipala/utils/utils.dart';\n\nclass RoutePush {\n  // 番剧跳转\n  static Future<void> bangumiPush(int? seasonId, int? epId,\n      {String? heroTag}) async {\n    SmartDialog.showLoading<dynamic>(msg: '获取中...');\n    try {\n      var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);\n      await SmartDialog.dismiss();\n      if (result['status']) {\n        if (result['data'].episodes.isEmpty) {\n          SmartDialog.showToast('资源获取失败');\n          return;\n        }\n        final BangumiInfoModel bangumiDetail = result['data'];\n        final EpisodeItem episode = bangumiDetail.episodes!.first;\n        final int epId = episode.id!;\n        final int cid = episode.cid!;\n        final String bvid = episode.bvid!;\n        final String cover = episode.cover!;\n        final Map arguments = <String, dynamic>{\n          'pic': cover,\n          'videoType': SearchType.media_bangumi,\n          // 'bangumiItem': bangumiDetail,\n        };\n        arguments['heroTag'] = heroTag ?? Utils.makeHeroTag(cid);\n        Get.toNamed(\n          '/video?bvid=$bvid&cid=$cid&epId=$epId',\n          arguments: arguments,\n        );\n      } else {\n        SmartDialog.showToast(result['msg']);\n      }\n    } catch (e) {\n      SmartDialog.showToast('番剧获取失败：$e');\n    }\n  }\n\n  // 登录跳转\n  static Future<void> loginPush() async {\n    await Get.toNamed(\n      '/webview',\n      parameters: {\n        'url': 'https://passport.bilibili.com/h5-app/passport/login',\n        'type': 'login',\n        'pageTitle': '登录bilibili',\n      },\n    );\n  }\n\n  // 登录跳转\n  static Future<void> loginRedirectPush() async {\n    await Get.offAndToNamed(\n      '/webview',\n      parameters: {\n        'url': 'https://passport.bilibili.com/h5-app/passport/login',\n        'type': 'login',\n        'pageTitle': '登录bilibili',\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/utils/storage.dart",
    "content": "import 'dart:io';\nimport 'package:hive_flutter/hive_flutter.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:pilipala/models/user/info.dart';\n\nclass GStrorage {\n  static late final Box<dynamic> userInfo;\n  static late final Box<dynamic> historyword;\n  static late final Box<dynamic> localCache;\n  static late final Box<dynamic> setting;\n  static late final Box<dynamic> video;\n\n  static Future<void> init() async {\n    final Directory dir = await getApplicationSupportDirectory();\n    final String path = dir.path;\n    await Hive.initFlutter('$path/hive');\n    regAdapter();\n    // 登录用户信息\n    userInfo = await Hive.openBox(\n      'userInfo',\n      compactionStrategy: (int entries, int deletedEntries) {\n        return deletedEntries > 2;\n      },\n    );\n    // 本地缓存\n    localCache = await Hive.openBox(\n      'localCache',\n      compactionStrategy: (int entries, int deletedEntries) {\n        return deletedEntries > 4;\n      },\n    );\n    // 设置\n    setting = await Hive.openBox('setting');\n    // 搜索历史\n    historyword = await Hive.openBox(\n      'historyWord',\n      compactionStrategy: (int entries, int deletedEntries) {\n        return deletedEntries > 10;\n      },\n    );\n    // 视频设置\n    video = await Hive.openBox('video');\n  }\n\n  static void regAdapter() {\n    Hive.registerAdapter(UserInfoDataAdapter());\n    Hive.registerAdapter(LevelInfoAdapter());\n  }\n\n  static Future<void> close() async {\n    // user.compact();\n    // user.close();\n    userInfo.compact();\n    userInfo.close();\n    historyword.compact();\n    historyword.close();\n    localCache.compact();\n    localCache.close();\n    setting.compact();\n    setting.close();\n    video.compact();\n    video.close();\n  }\n}\n\nclass SettingBoxKey {\n  /// 播放器\n  static const String btmProgressBehavior = 'btmProgressBehavior',\n      defaultVideoSpeed = 'defaultVideoSpeed',\n      autoUpgradeEnable = 'autoUpgradeEnable',\n      feedBackEnable = 'feedBackEnable',\n      defaultVideoQa = 'defaultVideoQa',\n      defaultLiveQa = 'defaultLiveQa',\n      defaultAudioQa = 'defaultAudioQa',\n      autoPlayEnable = 'autoPlayEnable',\n      fullScreenMode = 'fullScreenMode',\n      defaultDecode = 'defaultDecode',\n      danmakuEnable = 'danmakuEnable',\n      defaultToastOp = 'defaultToastOp',\n      defaultPicQa = 'defaultPicQa',\n      enableHA = 'enableHA',\n      enableOnlineTotal = 'enableOnlineTotal',\n      enableAutoBrightness = 'enableAutoBrightness',\n      enableAutoEnter = 'enableAutoEnter',\n      enableAutoExit = 'enableAutoExit',\n      p1080 = 'p1080',\n      enableCDN = 'enableCDN',\n      autoPiP = 'autoPiP',\n      enableAutoLongPressSpeed = 'enableAutoLongPressSpeed',\n      enablePlayerControlAnimation = 'enablePlayerControlAnimation',\n      // 默认音频输出方式\n      defaultAoOutput = 'defaultAoOutput',\n      // 港澳台模式\n      enableGATMode = 'enableGATMode',\n\n      // youtube 双击快进快退\n      enableQuickDouble = 'enableQuickDouble',\n      enableShowDanmaku = 'enableShowDanmaku',\n      enableBackgroundPlay = 'enableBackgroundPlay',\n      fullScreenGestureMode = 'fullScreenGestureMode',\n\n      /// 隐私\n      blackMidsList = 'blackMidsList',\n\n      /// 推荐\n      enableRcmdDynamic = 'enableRcmdDynamic',\n      defaultRcmdType = 'defaultRcmdType',\n      enableSaveLastData = 'enableSaveLastData',\n      minDurationForRcmd = 'minDurationForRcmd',\n      minLikeRatioForRecommend = 'minLikeRatioForRecommend',\n      exemptFilterForFollowed = 'exemptFilterForFollowed',\n      //filterUnfollowedRatio = 'filterUnfollowedRatio',\n      applyFilterToRelatedVideos = 'applyFilterToRelatedVideos',\n\n      /// 其他\n      autoUpdate = 'autoUpdate',\n      replySortType = 'replySortType',\n      defaultDynamicType = 'defaultDynamicType',\n      enableHotKey = 'enableHotKey',\n      enableQuickFav = 'enableQuickFav',\n      enableWordRe = 'enableWordRe',\n      enableSearchWord = 'enableSearchWord',\n      enableSystemProxy = 'enableSystemProxy',\n      enableAi = 'enableAi',\n      defaultHomePage = 'defaultHomePage',\n      enableRelatedVideo = 'enableRelatedVideo';\n\n  /// 外观\n  static const String themeMode = 'themeMode',\n      defaultTextScale = 'textScale',\n      dynamicColor = 'dynamicColor', // bool\n      customColor = 'customColor', // 自定义主题色\n      enableSingleRow = 'enableSingleRow', // 首页单列\n      displayMode = 'displayMode',\n      customRows = 'customRows', // 自定义列\n      enableMYBar = 'enableMYBar',\n      hideSearchBar = 'hideSearchBar', // 收起顶栏\n      hideTabBar = 'hideTabBar', // 收起底栏\n      tabbarSort = 'tabbarSort', // 首页tabbar\n      dynamicBadgeMode = 'dynamicBadgeMode',\n      enableGradientBg = 'enableGradientBg',\n      navBarSort = 'navBarSort',\n      actionTypeSort = 'actionTypeSort';\n}\n\nclass LocalCacheKey {\n  // 历史记录暂停状态 默认false 记录\n  static const String historyPause = 'historyPause',\n      // access_key\n      accessKey = 'accessKey',\n\n      //\n      wbiKeys = 'wbiKeys',\n      timeStamp = 'timeStamp',\n\n      // 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细\n      danmakuBlockType = 'danmakuBlockType',\n      danmakuShowArea = 'danmakuShowArea',\n      danmakuOpacity = 'danmakuOpacity',\n      danmakuFontScale = 'danmakuFontScale',\n      danmakuDuration = 'danmakuDuration',\n      strokeWidth = 'strokeWidth',\n\n      // 代理host port\n      systemProxyHost = 'systemProxyHost',\n      systemProxyPort = 'systemProxyPort';\n\n  static const String isDisableBatteryOptLocal = 'isDisableBatteryOptLocal',\n      isManufacturerBatteryOptimizationDisabled =\n          'isManufacturerBatteryOptimizationDisabled';\n}\n\nclass VideoBoxKey {\n  // 视频比例\n  static const String videoFit = 'videoFit',\n      // 亮度\n      videoBrightness = 'videoBrightness',\n      // 倍速\n      videoSpeed = 'videoSpeed',\n      // 播放顺序\n      playRepeat = 'playRepeat',\n      // 系统预设倍速\n      playSpeedSystem = 'playSpeedSystem',\n      // 默认倍速\n      playSpeedDefault = 'playSpeedDefault',\n      // 默认长按倍速\n      longPressSpeedDefault = 'longPressSpeedDefault',\n      // 自定义倍速集合\n      customSpeedsList = 'customSpeedsList',\n      // 画面填充比例\n      cacheVideoFit = 'cacheVideoFit';\n}\n"
  },
  {
    "path": "lib/utils/subtitle.dart",
    "content": "import 'dart:async';\nimport 'dart:isolate';\n\nclass SubTitleUtils {\n  // 格式整理\n  static Future<String> convertToWebVTT(List jsonData) async {\n    final receivePort = ReceivePort();\n    await Isolate.spawn(_convertToWebVTTIsolate, receivePort.sendPort);\n\n    final sendPort = await receivePort.first as SendPort;\n    final response = ReceivePort();\n    sendPort.send([jsonData, response.sendPort]);\n\n    return await response.first as String;\n  }\n\n  static void _convertToWebVTTIsolate(SendPort sendPort) async {\n    final port = ReceivePort();\n    sendPort.send(port.sendPort);\n\n    await for (final message in port) {\n      final List jsonData = message[0];\n      final SendPort replyTo = message[1];\n\n      String webVTTContent = 'WEBVTT FILE\\n\\n';\n      int chunkSize = 100; // 每次处理100条数据\n      int totalChunks = (jsonData.length / chunkSize).ceil();\n\n      for (int chunk = 0; chunk < totalChunks; chunk++) {\n        int start = chunk * chunkSize;\n        int end = start + chunkSize;\n        if (end > jsonData.length) end = jsonData.length;\n\n        for (int i = start; i < end; i++) {\n          final item = jsonData[i];\n          double from = double.parse(item['from'].toString());\n          double to = double.parse(item['to'].toString());\n          int sid = (item['sid'] ?? 0) as int;\n          String content = item['content'] as String;\n\n          webVTTContent += '$sid\\n';\n          webVTTContent += '${formatTime(from)} --> ${formatTime(to)}\\n';\n          webVTTContent += '$content\\n\\n';\n        }\n      }\n\n      replyTo.send(webVTTContent);\n    }\n  }\n\n  static String formatTime(num seconds) {\n    final String h = (seconds / 3600).floor().toString().padLeft(2, '0');\n    final String m = (seconds % 3600 / 60).floor().toString().padLeft(2, '0');\n    final String s = (seconds % 60).floor().toString().padLeft(2, '0');\n    final String ms =\n        (seconds * 1000 % 1000).floor().toString().padLeft(3, '0');\n    if (h == '00') {\n      return \"$m:$s.$ms\";\n    }\n    return \"$h:$m:$s.$ms\";\n  }\n}\n"
  },
  {
    "path": "lib/utils/url_utils.dart",
    "content": "import 'package:dio/dio.dart';\nimport 'package:get/get.dart';\n\nimport '../http/search.dart';\nimport 'id_utils.dart';\nimport 'utils.dart';\n\nclass UrlUtils {\n  // 302重定向路由截取\n  static Future<String> parseRedirectUrl(String url) async {\n    late String redirectUrl;\n    final dio = Dio();\n    dio.options.followRedirects = false;\n    dio.options.validateStatus = (status) {\n      return status == 200 || status == 301 || status == 302;\n    };\n    try {\n      final response = await dio.get(url);\n      if (response.statusCode == 302 || response.statusCode == 301) {\n        redirectUrl = response.headers['location']?.first as String;\n        if (redirectUrl.endsWith('/')) {\n          redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1);\n        }\n      } else {\n        if (url.endsWith('/')) {\n          url = url.substring(0, url.length - 1);\n        }\n        return url;\n      }\n      return redirectUrl;\n    } catch (err) {\n      return url;\n    }\n  }\n\n  // 匹配url路由跳转\n  static matchUrlPush(\n    String pathSegment,\n    String title,\n    String redirectUrl,\n  ) async {\n    final Map matchRes = IdUtils.matchAvorBv(input: pathSegment);\n    if (matchRes.containsKey('BV')) {\n      final String bv = matchRes['BV'];\n      final Map res = await SearchHttp.ab2cWithPic(bvid: bv);\n      final int cid = res['cid'];\n      final String? pic = res['pic'];\n      final String heroTag = Utils.makeHeroTag(bv);\n      await Get.toNamed(\n        '/video?bvid=$bv&cid=$cid',\n        arguments: <String, String?>{\n          'pic': pic,\n          'heroTag': heroTag,\n        },\n      );\n    } else {\n      await Get.toNamed(\n        '/webview',\n        parameters: {\n          'url': redirectUrl,\n          'type': 'url',\n          'pageTitle': title,\n        },\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/utils.dart",
    "content": "// 工具函数\n// ignore_for_file: non_constant_identifier_names\n\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:math';\nimport 'package:crypto/crypto.dart';\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_smart_dialog/flutter_smart_dialog.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport '../http/index.dart';\nimport '../models/github/latest.dart';\n\nclass Utils {\n  static final Random random = Random();\n\n  static Future<String> getCookiePath() async {\n    final Directory tempDir = await getApplicationSupportDirectory();\n    final String tempPath = \"${tempDir.path}/.plpl/\";\n    final Directory dir = Directory(tempPath);\n    final bool b = await dir.exists();\n    if (!b) {\n      dir.createSync(recursive: true);\n    }\n    return tempPath;\n  }\n\n  static String numFormat(dynamic number) {\n    if (number == null) {\n      return '0';\n    }\n    if (number is String) {\n      return number;\n    }\n    final String res = (number / 10000).toString();\n    if (int.parse(res.split('.')[0]) >= 1) {\n      return '${(number / 10000).toStringAsFixed(1)}万';\n    } else {\n      return number.toString();\n    }\n  }\n\n  static String timeFormat(dynamic time) {\n    // 1小时内\n    if (time is String && time.contains(':')) {\n      return time;\n    }\n    if (time < 3600) {\n      if (time == 0) {\n        return '00:00';\n      }\n      final int minute = time ~/ 60;\n      final double res = time / 60;\n      if (minute != res) {\n        return '${minute < 10 ? '0$minute' : minute}:${(time - minute * 60) < 10 ? '0${(time - minute * 60)}' : (time - minute * 60)}';\n      } else {\n        return '$minute:00';\n      }\n    } else {\n      final int hour = time ~/ 3600;\n      final String hourStr = hour < 10 ? '0$hour' : hour.toString();\n      var a = timeFormat(time - hour * 3600);\n      return '$hourStr:$a';\n    }\n  }\n\n  // 完全相对时间显示\n  static String formatTimestampToRelativeTime(timeStamp) {\n    var difference = DateTime.now()\n        .difference(DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000));\n\n    if (difference.inDays > 365) {\n      return '${difference.inDays ~/ 365}年前';\n    } else if (difference.inDays > 30) {\n      return '${difference.inDays ~/ 30}个月前';\n    } else if (difference.inDays > 0) {\n      return '${difference.inDays}天前';\n    } else if (difference.inHours > 0) {\n      return '${difference.inHours}小时前';\n    } else if (difference.inMinutes > 0) {\n      return '${difference.inMinutes}分钟前';\n    } else {\n      return '刚刚';\n    }\n  }\n\n  // 时间显示，刚刚，x分钟前\n  static String dateFormat(timeStamp, {formatType = 'list'}) {\n    if (timeStamp == 0 || timeStamp == null || timeStamp == '') {\n      return '';\n    }\n    // 当前时间\n    int time = (DateTime.now().millisecondsSinceEpoch / 1000).round();\n    // 对比\n    int distance = (time - timeStamp).toInt();\n    // 当前年日期\n    String currentYearStr = 'MM月DD日 hh:mm';\n    String lastYearStr = 'YY年MM月DD日 hh:mm';\n    if (formatType == 'detail') {\n      currentYearStr = 'MM-DD hh:mm';\n      lastYearStr = 'YY-MM-DD hh:mm';\n      return CustomStamp_str(\n          timestamp: timeStamp,\n          date: lastYearStr,\n          toInt: false,\n          formatType: formatType);\n    }\n    if (distance <= 60) {\n      return '刚刚';\n    } else if (distance <= 3600) {\n      return '${(distance / 60).floor()}分钟前';\n    } else if (distance <= 43200) {\n      return '${(distance / 60 / 60).floor()}小时前';\n    } else if (DateTime.fromMillisecondsSinceEpoch(time * 1000).year ==\n        DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000).year) {\n      return CustomStamp_str(\n          timestamp: timeStamp,\n          date: currentYearStr,\n          toInt: false,\n          formatType: formatType);\n    } else {\n      return CustomStamp_str(\n          timestamp: timeStamp,\n          date: lastYearStr,\n          toInt: false,\n          formatType: formatType);\n    }\n  }\n\n  // 时间戳转时间\n  static String CustomStamp_str(\n      {int? timestamp, // 为空则显示当前时间\n      String? date, // 显示格式，比如：'YY年MM月DD日 hh:mm:ss'\n      bool toInt = true, // 去除0开头\n      String? formatType}) {\n    timestamp ??= (DateTime.now().millisecondsSinceEpoch / 1000).round();\n    String timeStr =\n        (DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)).toString();\n\n    dynamic dateArr = timeStr.split(' ')[0];\n    dynamic timeArr = timeStr.split(' ')[1];\n\n    String YY = dateArr.split('-')[0];\n    String MM = dateArr.split('-')[1];\n    String DD = dateArr.split('-')[2];\n\n    String hh = timeArr.split(':')[0];\n    String mm = timeArr.split(':')[1];\n    String ss = timeArr.split(':')[2];\n\n    ss = ss.split('.')[0];\n\n    // 去除0开头\n    if (toInt) {\n      MM = (int.parse(MM)).toString();\n      DD = (int.parse(DD)).toString();\n      hh = (int.parse(hh)).toString();\n      mm = (int.parse(mm)).toString();\n    }\n\n    if (date == null) {\n      return timeStr;\n    }\n\n    // if (formatType == 'list' && int.parse(DD) > DateTime.now().day - 2) {\n    //   return '昨天';\n    // }\n\n    date = date\n        .replaceAll('YY', YY)\n        .replaceAll('MM', MM)\n        .replaceAll('DD', DD)\n        .replaceAll('hh', hh)\n        .replaceAll('mm', mm)\n        .replaceAll('ss', ss);\n    if (int.parse(YY) == DateTime.now().year &&\n        int.parse(MM) == DateTime.now().month) {\n      // 当天\n      if (int.parse(DD) == DateTime.now().day) {\n        return '今天';\n      }\n    }\n    return date;\n  }\n\n  static String makeHeroTag(v) {\n    return v.toString() + random.nextInt(9999).toString();\n  }\n\n  static int duration(String duration) {\n    List timeList = duration.split(':');\n    int len = timeList.length;\n    if (len == 2) {\n      return int.parse(timeList[0]) * 60 + int.parse(timeList[1]);\n    }\n    if (len == 3) {\n      return int.parse(timeList[0]) * 3600 +\n          int.parse(timeList[1]) * 60 +\n          int.parse(timeList[2]);\n    }\n    return 0;\n  }\n\n  static int findClosestNumber(int target, List<int> numbers) {\n    int minDiff = 127;\n    int closestNumber = 0; // 初始化为0，表示没有找到比目标值小的整数\n\n    if (numbers.contains(target)) {\n      return target;\n    }\n    // 向下查找\n    try {\n      for (int number in numbers) {\n        if (number < target) {\n          int diff = target - number; // 计算目标值与当前整数的差值\n          if (diff < minDiff) {\n            minDiff = diff;\n            closestNumber = number;\n            return closestNumber;\n          }\n        }\n      }\n    } catch (_) {}\n\n    // 向上查找\n    if (closestNumber == 0) {\n      try {\n        for (int number in numbers) {\n          int diff = (number - target).abs();\n\n          if (diff < minDiff) {\n            minDiff = diff;\n            closestNumber = number;\n          }\n        }\n      } catch (_) {}\n    }\n    return closestNumber;\n  }\n\n  // 版本对比\n  static bool needUpdate(localVersion, remoteVersion) {\n    List<String> localVersionList = localVersion.split('.');\n    List<String> remoteVersionList = remoteVersion.split('v')[1].split('.');\n    for (int i = 0; i < localVersionList.length; i++) {\n      int localVersion = int.parse(localVersionList[i]);\n      int remoteVersion = int.parse(remoteVersionList[i]);\n      if (remoteVersion > localVersion) {\n        return true;\n      } else if (remoteVersion < localVersion) {\n        return false;\n      }\n    }\n    return false;\n  }\n\n  // 检查更新\n  static Future<bool> checkUpdata() async {\n    SmartDialog.dismiss();\n    var currentInfo = await PackageInfo.fromPlatform();\n    var result = await Request().get(Api.latestApp, extra: {'ua': 'mob'});\n    if (result.data == null || result.data.isEmpty) {\n      SmartDialog.showToast('获取远程版本失败，请检查网络');\n      return false;\n    }\n    LatestDataModel data = LatestDataModel.fromJson(result.data);\n    bool isUpdate = Utils.needUpdate(currentInfo.version, data.tagName!);\n    if (isUpdate) {\n      SmartDialog.show(\n        animationType: SmartAnimationType.centerFade_otherSlide,\n        builder: (context) {\n          return AlertDialog(\n            title: const Text('🎉 发现新版本 '),\n            content: SizedBox(\n              height: 280,\n              child: SingleChildScrollView(\n                child: Column(\n                  mainAxisSize: MainAxisSize.min,\n                  mainAxisAlignment: MainAxisAlignment.start,\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      data.tagName!,\n                      style: const TextStyle(fontSize: 20),\n                    ),\n                    const SizedBox(height: 8),\n                    Text(data.body!),\n                  ],\n                ),\n              ),\n            ),\n            actions: [\n              TextButton(\n                onPressed: () => SmartDialog.dismiss(),\n                child: Text(\n                  '稍后',\n                  style:\n                      TextStyle(color: Theme.of(context).colorScheme.outline),\n                ),\n              ),\n              TextButton(\n                onPressed: () async {\n                  await SmartDialog.dismiss();\n                  launchUrl(\n                    Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'),\n                    mode: LaunchMode.externalApplication,\n                  );\n                },\n                child: const Text('网盘'),\n              ),\n              TextButton(\n                onPressed: () => matchVersion(data),\n                child: const Text('Github'),\n              ),\n            ],\n          );\n        },\n      );\n    }\n    return true;\n  }\n\n  // 下载适用于当前系统的安装包\n  static Future matchVersion(data) async {\n    await SmartDialog.dismiss();\n    // 获取设备信息\n    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();\n    if (Platform.isAndroid) {\n      AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;\n      // [arm64-v8a]\n      String abi = androidInfo.supportedAbis.first;\n      late String downloadUrl;\n      if (data.assets.isNotEmpty) {\n        for (var i in data.assets) {\n          if (i.downloadUrl.contains(abi)) {\n            downloadUrl = i.downloadUrl;\n          }\n        }\n        // 应用外下载\n        launchUrl(\n          Uri.parse(downloadUrl),\n          mode: LaunchMode.externalApplication,\n        );\n      }\n    }\n  }\n\n  // 时间戳转时间\n  static tampToSeektime(number) {\n    int hours = number ~/ 60;\n    int minutes = number % 60;\n\n    String formattedHours = hours.toString().padLeft(2, '0');\n    String formattedMinutes = minutes.toString().padLeft(2, '0');\n\n    return '$formattedHours:$formattedMinutes';\n  }\n\n  static String appSign(\n      Map<String, dynamic> params, String appkey, String appsec) {\n    params['appkey'] = appkey;\n    var searchParams = Uri(queryParameters: params).query;\n    var sortedParams = searchParams.split('&')..sort();\n    var sortedQueryString = sortedParams.join('&');\n\n    var appsecString = sortedQueryString + appsec;\n    var md5Digest = md5.convert(utf8.encode(appsecString));\n    var md5String = md5Digest.toString(); // 获取MD5哈希值\n\n    return md5String;\n  }\n\n  static List<int> generateRandomBytes(int minLength, int maxLength) {\n    return List<int>.generate(random.nextInt(maxLength - minLength + 1),\n        (_) => random.nextInt(0x60) + 0x20);\n  }\n\n  static String base64EncodeRandomString(int minLength, int maxLength) {\n    List<int> randomBytes = generateRandomBytes(minLength, maxLength);\n    return base64.encode(randomBytes);\n  }\n\n  static List<int> matchNum(String str) {\n    final RegExp regExp = RegExp(r'\\d+');\n    final Iterable<Match> matches = regExp.allMatches(str);\n\n    return matches.map((Match match) => int.parse(match.group(0)!)).toList();\n  }\n}\n"
  },
  {
    "path": "lib/utils/video_utils.dart",
    "content": "import 'package:pilipala/models/video/play/url.dart';\n\nimport '../models/live/room_info.dart';\n\nclass VideoUtils {\n  static String getCdnUrl(dynamic item) {\n    var backupUrl = \"\";\n    var videoUrl = \"\";\n\n    /// 先获取backupUrl 一般是upgcxcode地址 播放更稳定\n    if (item is VideoItem) {\n      backupUrl = item.backupUrl ?? \"\";\n      videoUrl = backupUrl.contains(\"http\") ? backupUrl : (item.baseUrl ?? \"\");\n    } else if (item is AudioItem) {\n      backupUrl = item.backupUrl ?? \"\";\n      videoUrl = backupUrl.contains(\"http\") ? backupUrl : (item.baseUrl ?? \"\");\n    } else if (item is CodecItem) {\n      backupUrl = (item.urlInfo?.first.host)! +\n          item.baseUrl! +\n          item.urlInfo!.first.extra!;\n      videoUrl = backupUrl.contains(\"http\") ? backupUrl : (item.baseUrl ?? \"\");\n    } else {\n      return \"\";\n    }\n\n    /// issues #70\n    if (videoUrl.contains(\".mcdn.bilivideo\")) {\n      videoUrl =\n          'https://proxy-tf-all-ws.bilivideo.com/?url=${Uri.encodeComponent(videoUrl)}';\n    } else if (videoUrl.contains(\"/upgcxcode/\")) {\n      //CDN列表\n      var cdnList = {\n        'ali': 'upos-sz-mirrorali.bilivideo.com',\n        'cos': 'upos-sz-mirrorcos.bilivideo.com',\n        'hw': 'upos-sz-mirrorhw.bilivideo.com',\n      };\n      //取一个CDN\n      var cdn = cdnList['ali'] ?? \"\";\n      var reg = RegExp(r'(http|https)://(.*?)/upgcxcode/');\n      videoUrl = videoUrl.replaceAll(reg, \"https://$cdn/upgcxcode/\");\n    }\n\n    return videoUrl;\n  }\n}\n"
  },
  {
    "path": "lib/utils/wbi_sign.dart",
    "content": "// Wbi签名 用于生成 REST API 请求中的 w_rid 和 wts 字段\n// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md\n// import md5 from 'md5'\n// import axios from 'axios'\nimport 'dart:convert';\nimport 'package:crypto/crypto.dart';\nimport 'package:hive/hive.dart';\nimport '../http/index.dart';\nimport 'storage.dart';\n\nclass WbiSign {\n  static Box<dynamic> localCache = GStrorage.localCache;\n  final List<int> mixinKeyEncTab = <int>[\n    46,\n    47,\n    18,\n    2,\n    53,\n    8,\n    23,\n    32,\n    15,\n    50,\n    10,\n    31,\n    58,\n    3,\n    45,\n    35,\n    27,\n    43,\n    5,\n    49,\n    33,\n    9,\n    42,\n    19,\n    29,\n    28,\n    14,\n    39,\n    12,\n    38,\n    41,\n    13,\n    37,\n    48,\n    7,\n    16,\n    24,\n    55,\n    40,\n    61,\n    26,\n    17,\n    0,\n    1,\n    60,\n    51,\n    30,\n    4,\n    22,\n    25,\n    54,\n    21,\n    56,\n    59,\n    6,\n    63,\n    57,\n    62,\n    11,\n    36,\n    20,\n    34,\n    44,\n    52\n  ];\n  // 对 imgKey 和 subKey 进行字符顺序打乱编码\n  String getMixinKey(String orig) {\n    String temp = '';\n    for (int i = 0; i < mixinKeyEncTab.length; i++) {\n      temp += orig.split('')[mixinKeyEncTab[i]];\n    }\n    return temp.substring(0, 32);\n  }\n\n  // 为请求参数进行 wbi 签名\n  Map<String, dynamic> encWbi(\n      Map<String, dynamic> params, String imgKey, String subKey) {\n    final String mixinKey = getMixinKey(imgKey + subKey);\n    final DateTime now = DateTime.now();\n    final int currTime = (now.millisecondsSinceEpoch / 1000).round();\n    final RegExp chrFilter = RegExp(r\"[!\\'\\(\\)*]\");\n    final List<String> query = <String>[];\n    final Map<String, dynamic> newParams = Map.from(params)\n      ..addAll({\"wts\": currTime}); // 添加 wts 字段\n    // 按照 key 重排参数\n    final List<String> keys = newParams.keys.toList()..sort();\n    for (String i in keys) {\n      query.add(\n          '${Uri.encodeComponent(i)}=${Uri.encodeComponent(newParams[i].toString().replaceAll(chrFilter, ''))}');\n    }\n    final String queryStr = query.join('&');\n    final String wbiSign =\n        md5.convert(utf8.encode(queryStr + mixinKey)).toString(); // 计算 w_rid\n    return {'wts': currTime.toString(), 'w_rid': wbiSign};\n  }\n\n  // 获取最新的 img_key 和 sub_key 可以从缓存中获取\n  static Future<Map<String, dynamic>> getWbiKeys() async {\n    final DateTime nowDate = DateTime.now();\n    if (localCache.get(LocalCacheKey.wbiKeys) != null &&\n        DateTime.fromMillisecondsSinceEpoch(\n                    localCache.get(LocalCacheKey.timeStamp) as int)\n                .day ==\n            nowDate.day) {\n      final Map cacheWbiKeys = localCache.get('wbiKeys');\n      return Map<String, dynamic>.from(cacheWbiKeys);\n    }\n    var resp =\n        await Request().get('https://api.bilibili.com/x/web-interface/nav');\n    var jsonContent = resp.data['data'];\n\n    final String imgUrl = jsonContent['wbi_img']['img_url'];\n    final String subUrl = jsonContent['wbi_img']['sub_url'];\n    final Map<String, dynamic> wbiKeys = {\n      'imgKey': imgUrl\n          .substring(imgUrl.lastIndexOf('/') + 1, imgUrl.length)\n          .split('.')[0],\n      'subKey': subUrl\n          .substring(subUrl.lastIndexOf('/') + 1, subUrl.length)\n          .split('.')[0]\n    };\n    localCache.put(LocalCacheKey.wbiKeys, wbiKeys);\n    localCache.put(LocalCacheKey.timeStamp, nowDate.millisecondsSinceEpoch);\n    return wbiKeys;\n  }\n\n  Future<Map<String, dynamic>> makSign(Map<String, dynamic> params) async {\n    // params 为需要加密的请求参数\n    final Map<String, dynamic> wbiKeys = await getWbiKeys();\n    final Map<String, dynamic> query = params\n      ..addAll(encWbi(params, wbiKeys['imgKey'], wbiKeys['subKey']));\n    return query;\n  }\n}\n"
  },
  {
    "path": "linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "linux/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"pilipala\")\n# The unique GTK application identifier for this application. See:\n# https://wiki.gnome.org/HowDoI/ChooseApplicationID\nset(APPLICATION_ID \"com.example.pilipala\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Load bundled libraries from the lib/ directory relative to the binary.\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Define build configuration options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Define the application target. To change its name, change BINARY_NAME above,\n# not the value here, or `flutter run` will no longer work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add dependency libraries. Add any application-specific dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nforeach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})\n  install(FILES \"${bundled_library}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendforeach(bundled_library)\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\nif(NOT CMAKE_BUILD_TYPE MATCHES \"Debug\")\n  install(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n"
  },
  {
    "path": "linux/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\n\n# Serves the same purpose as list(TRANSFORM ... PREPEND ...),\n# which isn't available in 3.10.\nfunction(list_prepend LIST_NAME PREFIX)\n    set(NEW_LIST \"\")\n    foreach(element ${${LIST_NAME}})\n        list(APPEND NEW_LIST \"${PREFIX}${element}\")\n    endforeach(element)\n    set(${LIST_NAME} \"${NEW_LIST}\" PARENT_SCOPE)\nendfunction()\n\n# === Flutter Library ===\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\npkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)\npkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)\n\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/libflutter_linux_gtk.so\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/lib/libapp.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"fl_basic_message_channel.h\"\n  \"fl_binary_codec.h\"\n  \"fl_binary_messenger.h\"\n  \"fl_dart_project.h\"\n  \"fl_engine.h\"\n  \"fl_json_message_codec.h\"\n  \"fl_json_method_codec.h\"\n  \"fl_message_codec.h\"\n  \"fl_method_call.h\"\n  \"fl_method_channel.h\"\n  \"fl_method_codec.h\"\n  \"fl_method_response.h\"\n  \"fl_plugin_registrar.h\"\n  \"fl_plugin_registry.h\"\n  \"fl_standard_message_codec.h\"\n  \"fl_standard_method_codec.h\"\n  \"fl_string_codec.h\"\n  \"fl_value.h\"\n  \"fl_view.h\"\n  \"flutter_linux.h\"\n)\nlist_prepend(FLUTTER_LIBRARY_HEADERS \"${EPHEMERAL_DIR}/flutter_linux/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}\")\ntarget_link_libraries(flutter INTERFACE\n  PkgConfig::GTK\n  PkgConfig::GLIB\n  PkgConfig::GIO\n)\nadd_dependencies(flutter flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CMAKE_CURRENT_BINARY_DIR}/_phony_\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh\"\n      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n)\n"
  },
  {
    "path": "linux/main.cc",
    "content": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  return g_application_run(G_APPLICATION(app), argc, argv);\n}\n"
  },
  {
    "path": "linux/my_application.cc",
    "content": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#endif\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nstruct _MyApplication {\n  GtkApplication parent_instance;\n  char** dart_entrypoint_arguments;\n};\n\nG_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)\n\n// Implements GApplication::activate.\nstatic void my_application_activate(GApplication* application) {\n  MyApplication* self = MY_APPLICATION(application);\n  GtkWindow* window =\n      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\n\n  // Use a header bar when running in GNOME as this is the common style used\n  // by applications and is the setup most users will be using (e.g. Ubuntu\n  // desktop).\n  // If running on X and not using GNOME then just use a traditional title bar\n  // in case the window manager does more exotic layout, e.g. tiling.\n  // If running on Wayland assume the header bar will work (may need changing\n  // if future cases occur).\n  gboolean use_header_bar = TRUE;\n#ifdef GDK_WINDOWING_X11\n  GdkScreen* screen = gtk_window_get_screen(window);\n  if (GDK_IS_X11_SCREEN(screen)) {\n    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);\n    if (g_strcmp0(wm_name, \"GNOME Shell\") != 0) {\n      use_header_bar = FALSE;\n    }\n  }\n#endif\n  if (use_header_bar) {\n    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());\n    gtk_widget_show(GTK_WIDGET(header_bar));\n    gtk_header_bar_set_title(header_bar, \"pilipala\");\n    gtk_header_bar_set_show_close_button(header_bar, TRUE);\n    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));\n  } else {\n    gtk_window_set_title(window, \"pilipala\");\n  }\n\n  gtk_window_set_default_size(window, 1280, 720);\n  gtk_widget_show(GTK_WIDGET(window));\n\n  g_autoptr(FlDartProject) project = fl_dart_project_new();\n  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\n\n  FlView* view = fl_view_new(project);\n  gtk_widget_show(GTK_WIDGET(view));\n  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\n\n  fl_register_plugins(FL_PLUGIN_REGISTRY(view));\n\n  gtk_widget_grab_focus(GTK_WIDGET(view));\n}\n\n// Implements GApplication::local_command_line.\nstatic gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {\n  MyApplication* self = MY_APPLICATION(application);\n  // Strip out the first argument as it is the binary name.\n  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);\n\n  g_autoptr(GError) error = nullptr;\n  if (!g_application_register(application, nullptr, &error)) {\n     g_warning(\"Failed to register: %s\", error->message);\n     *exit_status = 1;\n     return TRUE;\n  }\n\n  g_application_activate(application);\n  *exit_status = 0;\n\n  return TRUE;\n}\n\n// Implements GObject::dispose.\nstatic void my_application_dispose(GObject* object) {\n  MyApplication* self = MY_APPLICATION(object);\n  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);\n  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);\n}\n\nstatic void my_application_class_init(MyApplicationClass* klass) {\n  G_APPLICATION_CLASS(klass)->activate = my_application_activate;\n  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;\n  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;\n}\n\nstatic void my_application_init(MyApplication* self) {}\n\nMyApplication* my_application_new() {\n  return MY_APPLICATION(g_object_new(my_application_get_type(),\n                                     \"application-id\", APPLICATION_ID,\n                                     \"flags\", G_APPLICATION_NON_UNIQUE,\n                                     nullptr));\n}\n"
  },
  {
    "path": "linux/my_application.h",
    "content": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,\n                     GtkApplication)\n\n/**\n * my_application_new:\n *\n * Creates a new Flutter-based application.\n *\n * Returns: a new #MyApplication.\n */\nMyApplication* my_application_new();\n\n#endif  // FLUTTER_MY_APPLICATION_H_\n"
  },
  {
    "path": "macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/dgph\n**/xcuserdata/\n"
  },
  {
    "path": "macos/Flutter/Flutter-Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Podfile",
    "content": "platform :osx, '10.14'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \\\"flutter pub get\\\" is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \\\"flutter pub get\\\"\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_macos_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_macos_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@NSApplicationMain\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return true\n  }\n}\n"
  },
  {
    "path": "macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n    \"info\": {\n        \"version\": 1,\n        \"author\": \"xcode\"\n    },\n    \"images\": [\n        {\n            \"size\": \"16x16\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_16.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"16x16\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_32.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"32x32\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_32.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"32x32\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_64.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"128x128\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_128.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"128x128\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_256.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"256x256\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_256.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"256x256\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_512.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"512x512\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_512.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"512x512\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_1024.png\",\n            \"scale\": \"2x\"\n        }\n    ]\n}"
  },
  {
    "path": "macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"14490.70\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"14490.70\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                            <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"pasteAsPlainText:\" target=\"-1\" id=\"cEh-KX-wJQ\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                    <items>\n                                        <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"cD7-Qs-BN4\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"WD3-Gg-5AJ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"NDo-RZ-v9R\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"HOh-sY-3ay\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"U76-nv-p5D\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                            <connections>\n                                                <action selector=\"centerSelectionInVisibleArea:\" target=\"-1\" id=\"IOG-6D-g5B\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                    <items>\n                                        <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                            <connections>\n                                                <action selector=\"showGuessPanel:\" target=\"-1\" id=\"vFj-Ks-hy3\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                            <connections>\n                                                <action selector=\"checkSpelling:\" target=\"-1\" id=\"fz7-VC-reM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                        <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleContinuousSpellChecking:\" target=\"-1\" id=\"7w6-Qz-0kB\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleGrammarChecking:\" target=\"-1\" id=\"muD-Qn-j4w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"-1\" id=\"2lM-Qi-WAP\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                    <items>\n                                        <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontSubstitutionsPanel:\" target=\"-1\" id=\"oku-mr-iSq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                        <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleSmartInsertDelete:\" target=\"-1\" id=\"3IJ-Se-DZD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"-1\" id=\"ptq-xd-QOA\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDashSubstitution:\" target=\"-1\" id=\"oCt-pO-9gS\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticLinkDetection:\" target=\"-1\" id=\"Gip-E3-Fov\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDataDetection:\" target=\"-1\" id=\"R1I-Nq-Kbl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticTextReplacement:\" target=\"-1\" id=\"DvP-Fe-Py6\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                        <items>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Help\" id=\"EPT-qC-fAb\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"rJ0-wn-3NY\"/>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"2560\" height=\"1577\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = pilipala\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = com.example.pilipala\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved.\n"
  },
  {
    "path": "macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController.init()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n}\n"
  },
  {
    "path": "macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n\t\t70B7435992536DF7EF916102 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA6434EEFB6EABF56163956B /* Pods_Runner.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* pilipala.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = pilipala.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t55ED1760F98F03A067237962 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\tA5BA27756D8018CEC961D98E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.debug.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tBA6434EEFB6EABF56163956B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tC7AC3B7DF8D09FAFBD7AC6AD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.release.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t70B7435992536DF7EF916102 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t\t3BC3551DE517F6B6D0C82543 /* Pods */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* pilipala.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3BC3551DE517F6B6D0C82543 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tA5BA27756D8018CEC961D98E /* Pods-Runner.debug.xcconfig */,\n\t\t\t\tC7AC3B7DF8D09FAFBD7AC6AD /* Pods-Runner.release.xcconfig */,\n\t\t\t\t55ED1760F98F03A067237962 /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tname = Pods;\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tBA6434EEFB6EABF56163956B /* Pods_Runner.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t5190B88ACAD3AAF5B1766AE1 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t\t02C468425C99B15D8DFFA40D /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* pilipala.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1300;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t02C468425C99B15D8DFFA40D /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n\t\t5190B88ACAD3AAF5B1766AE1 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1300\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"pilipala.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"pilipala.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"pilipala.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"pilipala.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "macos/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: pilipala\ndescription: A new Flutter project.\n# The following line prevents the package from being accidentally published to\n# pub.dev using `flutter pub publish`. This is preferred for private packages.\npublish_to: \"none\" # Remove this line if you wish to publish to pub.dev\n\n# The following defines the version and build number for your application.\n# A version number is three numbers separated by dots, like 1.2.43\n# followed by an optional build number separated by a +.\n# Both the version and the builder number may be overridden in flutter\n# build by specifying --build-name and --build-number, respectively.\n# In Android, build-name is used as versionName while build-number used as versionCode.\n# Read more about Android versioning at https://developer.android.com/studio/publish/versioning\n# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.\n# Read more about iOS versioning at\n# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html\n# In Windows, build-name is used as the major, minor, and patch parts\n# of the product and file versions while build-number is used as the build suffix.\nversion: 1.0.25+1025\n\nenvironment:\n  sdk: \">=3.0.0 <4.0.0\"\n\n# Dependencies specify other packages that your package needs in order to work.\n# To automatically upgrade your package dependencies to the latest versions\n# consider running `flutter pub upgrade --major-versions`. Alternatively,\n# dependencies can be manually updated by changing the version numbers below to\n# the latest version available on pub.dev. To see which dependencies have newer\n# versions available, run `flutter pub outdated`.\ndependencies:\n  flutter:\n    sdk: flutter\n  flutter_localizations:\n    sdk: flutter\n  # The following adds the Cupertino Icons font to your application.\n  # Use with the CupertinoIcons class for iOS style icons.\n  cupertino_icons: ^1.0.5\n  # 动态取色\n  dynamic_color: ^1.7.0\n\n  get: ^4.6.5\n\n  # 网络\n  dio: ^5.4.1\n  cookie_jar: ^4.0.8\n  dio_cookie_manager: ^3.1.1\n  connectivity_plus: ^6.0.3\n  dio_http2_adapter: ^2.3.1+1\n\n  # 图片\n  cached_network_image: ^3.3.0\n  saver_gallery: ^3.0.1\n\n  # 存储\n  path_provider: ^2.1.1\n  hive: ^2.2.3\n  hive_flutter: ^1.1.0\n\n  # 设备信息\n  device_info_plus: ^9.0.0\n  # 权限\n  permission_handler: ^11.3.0\n  # 分享\n  share_plus: ^7.0.2\n  # cookie 管理\n  webview_cookie_manager: ^2.0.6\n  # 浏览器\n  webview_flutter: ^4.8.0\n  # 解决sliver滑动不同步\n  extended_nested_scroll_view: ^6.2.1\n  # 上拉加载\n  loading_more_list: ^6.0.0\n  # 下拉刷新\n  pull_to_refresh_notification: ^3.0.1\n  # 图标\n  font_awesome_flutter: ^10.4.0\n  # toast\n  flutter_smart_dialog: ^4.9.4\n  # 下滑关闭\n  dismissible_page: ^1.0.2\n  custom_sliding_segmented_control: ^1.8.3\n  # 加密\n  crypto: ^3.0.3\n  encrypt: ^5.0.3\n\n  # 视频播放器\n  media_kit: ^1.1.10 # Primary package.\n  media_kit_video: ^1.2.4 # For video rendering.\n  media_kit_libs_video: ^1.0.4\n\n  # 媒体通知\n  audio_service: ^0.18.13\n  audio_session: ^0.1.18\n\n  # 音量、亮度、屏幕控制\n  flutter_volume_controller: ^1.3.2\n  screen_brightness: ^0.2.2+1\n  wakelock_plus: ^1.1.6\n  universal_platform: ^1.1.0\n  # 进度条\n  audio_video_progress_bar: ^2.0.3\n  auto_orientation: ^2.3.1\n  protobuf: ^3.0.0\n\n  # 获取appx信息\n  package_info_plus: ^4.2.0\n  url_launcher: ^6.2.6\n  flutter_svg: ^2.0.10+1\n  # 防抖节流\n  easy_debounce: ^2.0.3\n  # 高帧率\n  flutter_displaymode: ^0.6.0\n  # scheme跳转\n  appscheme: ^1.0.8\n  # 弹幕\n  ns_danmaku:\n    git:\n      url: https://github.com/guozhigq/flutter_ns_danmaku.git\n      ref: master\n  # 状态栏图标控制\n  status_bar_control: ^3.2.1\n  # 代理\n  system_proxy: ^0.1.0\n  # pip\n  floating:\n    git:\n      url: https://github.com/guozhigq/floating.git\n      ref: main\n  # html解析\n  html: ^0.15.4\n  # html渲染\n  flutter_html: ^3.0.0-beta.2\n  # 极验\n  gt3_flutter_plugin: ^0.1.0\n  uuid: ^3.0.7\n  scrollable_positioned_list: ^0.3.8\n  catcher_2: ^1.2.6\n  logger: ^2.3.0\n  path: ^1.9.0\n  # 电池优化\n  disable_battery_optimization: ^1.1.1\n  # 展开/收起\n  expandable: ^5.0.1\n  # 投屏\n  dlna_dart: ^0.0.8\n  lottie: ^3.1.2\n  # 二维码\n  qr_flutter: ^4.1.0\n  bottom_sheet: ^4.0.4\n  web_socket_channel: ^2.4.5\n  brotli: ^0.6.0\n  # 文本语法高亮\n  re_highlight: ^0.0.3\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n  # The \"flutter_lints\" package below contains a set of recommended lints to\n  # encourage good coding practices. The lint set provided by the package is\n  # activated in the `analysis_options.yaml` file located at the root of your\n  # package. See that file for information about deactivating specific lint\n  # rules and activating additional ones.\n  flutter_lints: ^2.0.0\n  flutter_launcher_icons: ^0.13.1\n  # flutter_launcher_icons:\n  #   git:\n  #     url: https://github.com/nvi9/flutter_launcher_icons.git\n  #     ref: e045d40\n  hive_generator: ^2.0.0\n  build_runner: ^2.4.8\n\ndependency_overrides:\n  media_kit:\n    git:\n      url: https://github.com/media-kit/media-kit\n      path: media_kit\n  media_kit_video:\n    git:\n      url: https://github.com/media-kit/media-kit\n      path: media_kit_video\n  media_kit_libs_video:\n    git:\n      url: https://github.com/media-kit/media-kit\n      path: libs/universal/media_kit_libs_video\n\nflutter_launcher_icons:\n  android: true\n  ios: true\n  remove_alpha_ios: true\n  image_path: assets/images/logo/logo_android_2.png\n  image_path_android: assets/images/logo/logo_android_2.png\n  image_path_ios: assets/images/logo/logo_ios.png\n  adaptive_icon_background: \"#ffffff\"\n  adaptive_icon_foreground: assets/images/logo/logo_android_2.png\n  adaptive_icon_monochrome: assets/images/logo/logo_android_2.png\n  macos:\n    generate: true\n    image_path: assets/images/logo/logo_ios.png\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\n# The following section is specific to Flutter packages.\nflutter:\n  # The following line ensures that the Material Icons font is\n  # included with your application, so that you can use the icons in\n  # the material Icons class.\n  uses-material-design: true\n\n  # To add assets to your application, add an assets section, like this:\n  assets:\n    - assets/\n    - assets/images/\n    - assets/images/lv/\n    - assets/images/logo/\n    - assets/images/live/\n    - assets/images/video/\n  # An image asset can refer to one or more resolution-specific \"variants\", see\n  # https://flutter.dev/assets-and-images/#resolution-aware\n\n  # For details regarding adding assets from package dependencies, see\n  # https://flutter.dev/assets-and-images/#from-packages\n\n  # To add custom fonts to your application, add a fonts section here,\n  # in this \"flutter\" section. Each entry in this list should have a\n  # \"family\" key with the font family name, and a \"fonts\" key with a\n  # list giving the asset and other descriptors for the font. For\n  # example:\n  fonts:\n    - family: fansCard\n      fonts:\n        - asset: assets/fonts/fansCard.ttf\n    - family: Jura-Bold\n      fonts:\n        - asset: assets/fonts/Jura-Bold.ttf\n    # - family: HarmonyOS\n    #   fonts:\n    #     - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf\n\n  # For details regarding fonts from package dependencies,\n  # see https://flutter.dev/custom-fonts/#from-packages\n"
  },
  {
    "path": "test/widget_test.dart",
    "content": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility in the flutter_test package. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'package:pilipala/main.dart';\n\nvoid main() {\n  testWidgets('Counter increments smoke test', (WidgetTester tester) async {\n    // Build our app and trigger a frame.\n    await tester.pumpWidget(const MyApp());\n\n    // Verify that our counter starts at 0.\n    expect(find.text('0'), findsOneWidget);\n    expect(find.text('1'), findsNothing);\n\n    // Tap the '+' icon and trigger a frame.\n    await tester.tap(find.byIcon(Icons.add));\n    await tester.pump();\n\n    // Verify that our counter has incremented.\n    expect(find.text('0'), findsNothing);\n    expect(find.text('1'), findsOneWidget);\n  });\n}\n"
  },
  {
    "path": "web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A new Flutter project.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"pilipala\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <!-- Favicon -->\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\"/>\n\n  <title>pilipala</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n\n  <script>\n    // The value below is injected by flutter build, do not touch.\n    var serviceWorkerVersion = null;\n  </script>\n  <!-- This script adds the flutter initialization JS code -->\n  <script src=\"flutter.js\" defer></script>\n</head>\n<body>\n  <script>\n    window.addEventListener('load', function(ev) {\n      // Download main.dart.js\n      _flutter.loader.loadEntrypoint({\n        serviceWorker: {\n          serviceWorkerVersion: serviceWorkerVersion,\n        },\n        onEntrypointLoaded: function(engineInitializer) {\n          engineInitializer.initializeEngine().then(function(appRunner) {\n            appRunner.runApp();\n          });\n        }\n      });\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "web/manifest.json",
    "content": "{\n    \"name\": \"pilipala\",\n    \"short_name\": \"pilipala\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"A new Flutter project.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": [\n        {\n            \"src\": \"icons/Icon-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        }\n    ]\n}\n"
  },
  {
    "path": "windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "windows/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.14)\nproject(pilipala LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"pilipala\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Define build configuration option.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n# Define settings for the Profile build mode.\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build; see runner/CMakeLists.txt.\nadd_subdirectory(\"runner\")\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "windows/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      windows-x64 $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name, change BINARY_NAME in the\n# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer\n# work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add preprocessor definitions for the build version.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION=\\\"${FLUTTER_VERSION}\\\"\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}\")\n\n# Disable Windows macros that collide with C++ standard library functions.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\n\n# Add dependency libraries and include directories. Add any application-specific\n# dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_link_libraries(${BINARY_NAME} PRIVATE \"dwmapi.lib\")\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n#else\n#define VERSION_AS_NUMBER 1,0,0,0\n#endif\n\n#if defined(FLUTTER_VERSION)\n#define VERSION_AS_STRING FLUTTER_VERSION\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.example\" \"\\0\"\n            VALUE \"FileDescription\", \"pilipala\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"pilipala\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2023 com.example. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"pilipala.exe\" \"\\0\"\n            VALUE \"ProductName\", \"pilipala\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n\n  flutter_controller_->engine()->SetNextFrameCallback([&]() {\n    this->Show();\n  });\n\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.Create(L\"pilipala\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0)) {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 and Windows 11 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr);\n  std::string utf8_string;\n  if (target_length == 0 || target_length > utf8_string.max_size()) {\n    return utf8_string;\n  }\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, utf8_string.data(),\n      target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <dwmapi.h>\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\n/// Window attribute that enables dark mode window decorations.\n///\n/// Redefined in case the developer's machine has a Windows SDK older than\n/// version 10.0.22000.0.\n/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute\n#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE\n#define DWMWA_USE_IMMERSIVE_DARK_MODE 20\n#endif\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n/// Registry key for app theme preference.\n///\n/// A value of 0 indicates apps should use dark mode. A non-zero or missing\n/// value indicates apps should use light mode.\nconstexpr const wchar_t kGetPreferredBrightnessRegKey[] =\n  L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\nconstexpr const wchar_t kGetPreferredBrightnessRegValue[] = L\"AppsUseLightTheme\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n  }\n  FreeLibrary(user32_module);\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::Create(const std::wstring& title,\n                         const Point& origin,\n                         const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  UpdateTheme(window);\n\n  return OnCreate();\n}\n\nbool Win32Window::Show() {\n  return ShowWindow(window_handle_, SW_SHOWNORMAL);\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n\n    case WM_DWMCOLORIZATIONCOLORCHANGED:\n      UpdateTheme(hwnd);\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n\nvoid Win32Window::UpdateTheme(HWND const window) {\n  DWORD light_mode;\n  DWORD light_mode_size = sizeof(light_mode);\n  LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,\n                               kGetPreferredBrightnessRegValue,\n                               RRF_RT_REG_DWORD, nullptr, &light_mode,\n                               &light_mode_size);\n\n  if (result == ERROR_SUCCESS) {\n    BOOL enable_dark_mode = light_mode == 0;\n    DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,\n                          &enable_dark_mode, sizeof(enable_dark_mode));\n  }\n}\n"
  },
  {
    "path": "windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates a win32 window with |title| that is positioned and sized using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size this function will scale the inputted width and height as\n  // as appropriate for the default monitor. The window is invisible until\n  // |Show| is called. Returns true if the window was created successfully.\n  bool Create(const std::wstring& title, const Point& origin, const Size& size);\n\n  // Show the current window. Returns true if the window was successfully shown.\n  bool Show();\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responsponds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  // Update the window frame's theme to match the system theme.\n  static void UpdateTheme(HWND const window);\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  }
]